這是近期工作上遇到的問題,記錄一下原因與解法,解法不一定是最佳解。
問題背景
接手了公司的一個網站,發現使用者一直在此單頁應用 SPA 中遇到偶發、但令人頭痛的問題。通常會在網站「部署新版本」後、且「使用者尚未重新整理頁面」的情境下發生。
情境如下:
- 使用者正在我們的後台網站上瀏覽(例如還停留在
/home
)。 - 這時我們開發者部署了一個新版本,且採用了「覆蓋式部署」(也就是直接覆蓋掉原本的靜態資源)。
- 使用者繼續使用網站並透過前端路由導覽到尚未載入過的頁面(例如
/profile
),這個頁面會觸發懶加載動作(例如Profile.chunk.js
)。 - 由於使用者當前的 HTML 是舊版本,因此所引用的 chunk 路徑還是舊版本的,如
static/js/Profile.abcd1234.chunk.js
。 - 但這個資源已經在部署新版本時被移除或覆蓋,所以會出現 404,導致頁面無法顯示..。
這就是為什麼有些頁面可以載入、有些不行的原因 ,因為已載入的 chunk 檔案還在快取裡,沒載過的就掛了 QQ
而當使用者重新整理頁面時,會重新請求最新版本的 HTML 檔案,新的 chunk 資訊就會對應到最新版本的資源,自然就沒問題了。
快速解法:出錯時自動重新整理
這個做法最粗暴,但使用上要很小心,就是當資源載入錯誤時,強制 reload 更新頁面。
window.addEventListener('error', (event) => {
const target = event.target as HTMLScriptElement;
if (target.tagName === 'SCRIPT' && target.src.includes('chunk')) {
// 嘗試限制最多重整 3 次,避免無限迴圈
const retryCount = Number(sessionStorage.getItem('RETRY_COUNT') || '0');
if (retryCount < 3) {
sessionStorage.setItem('RETRY_COUNT', (retryCount + 1).toString());
location.reload();
}
}
}, true);
直接 reload 更新可能會造成無限循環(例如資源還是找不到),所以最好搭配「重試次數」與「重試間隔」的邏輯來控制風險。
相對好解法:靜態資源帶版本 + 保留歷史版本
要避免這種資源失效的問題,最佳做法就是部署時保留舊版本,並讓每個版本的資源獨立存在。
如何實作版本化路徑?
例如我們可以將靜態資源的路徑結構調整如下:
原本:
/static/js/bundle.abcd1234.js
調整後:
/v1.0.0/static/js/bundle.abcd1234.js
如此一來,不同版本之間的靜態資源互不干擾。
部署
我們可以在 web 伺服器(例如我們公司用到了 Nginx)的根目錄下建立以版本號命名的子資料夾,例如
/var/www/my-app/
├── v1.0.0/
├── v1.1.0/
└── index.html
在部署新版本時:
- 將新資源打包到新的版本目錄中。
- 設定 index.html 透過某種方式引用對應版本的資源。
如果不想讓版本號顯示在 URL 中,可以:使用 hash 來模糊版本資訊,如 /static/8f92j4/static.js