這是近期工作上遇到的問題,記錄一下原因與解法,解法不一定是最佳解。

問題背景

接手了公司的一個網站,發現使用者一直在此單頁應用 SPA 中遇到偶發、但令人頭痛的問題。通常會在網站「部署新版本」後、且「使用者尚未重新整理頁面」的情境下發生。

情境如下:

  1. 使用者正在我們的後台網站上瀏覽(例如還停留在 /home)。
  2. 這時我們開發者部署了一個新版本,且採用了「覆蓋式部署」(也就是直接覆蓋掉原本的靜態資源)。
  3. 使用者繼續使用網站並透過前端路由導覽到尚未載入過的頁面(例如 /profile),這個頁面會觸發懶加載動作(例如 Profile.chunk.js)。
  4. 由於使用者當前的 HTML 是舊版本,因此所引用的 chunk 路徑還是舊版本的,如 static/js/Profile.abcd1234.chunk.js
  5. 但這個資源已經在部署新版本時被移除或覆蓋,所以會出現 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