🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

===============================

場景

--

在單頁應用(SPA)專案中,有一個問題非常常見,但又經常被低估:系統明明已經發佈了新版本,部分使用者卻依然停留在舊頁面中繼續操作

大多數時候,這種狀態並不會立刻出問題,所以團隊往往不太在意。但一旦使用者繼續進行路由跳轉、訪問懶載入頁面,或者觸發某些依賴新資源的操作,就可能出現下面這些現象:

  • 頁面跳轉失敗
  • 控制台出現 Loading chunk failed
  • Failed to fetch dynamically imported module
  • 頁面局部報錯,甚至直接白屏
  • 使用者不知道系統已經更新,只會覺得「網頁壞了」
  • 新功能已經上線,但使用者卻遲遲體驗不到

這類問題在線上系統裡並不少見,尤其是管理後台、教學平台、營運平台這類使用者會長時間掛著頁面不重新整理的 SPA 應用。 如果處理不好,不僅影響使用者體驗,還會帶來很多「難定位、難重現」的線上問題。

這篇文章,我想系統說清楚三件事:

  1. 為什麼 SPA 發版後,舊頁面容易出問題
  2. 這類白屏問題的根因到底是什麼
  3. 如何從前端執行時、快取策略和部署方式三個層面,設計一套完整的解決方案

一、把問題流程拆開看,就很清楚了

這個問題的完整鏈路可以概括為:

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span>使用者打開舊頁面</span>
<span>   ↓</span>
<span>系統發佈新版本</span>
<span>   ↓</span>
<span>使用者仍然停留在舊頁面中</span>
<span>   ↓</span>
<span>使用者觸發路由跳轉 / 懶載入頁面</span>
<span>   ↓</span>
<span>瀏覽器請求某個 chunk 資源</span>
<span>   ↓</span>
<span>舊資源已失效或請求地址不匹配</span>
<span>   ↓</span>
<span>動態 <span>import</span> 失敗</span>
<span>   ↓</span>
<span>頁面報錯、跳轉失敗甚至白屏</span>

也就是,這不是某一個孤立的 bug,而是一個典型的**版本切換時機問題**。

---

二、解決思路:從三個層面一起治理
----------------

這個問題不能只靠某一個點狀方案解決。更合理的方式,是從以下三個層面同時考慮:

### 1. 讓使用者知道「線上有新版本了」「建議重新整理頁面」

也就是建立**版本檢測機制**、**更新提示機制**。

### 2. 在資源載入失敗時自動自救

也就是建立**chunk 載入失敗兜底機制**。

### 3. 從快取層降低問題發生機率

也就是建立**快取與發佈治理策略**。

下面分別展開說。

方案一:建立版本檢測機制
------------

整個機制可以這樣設計:

1. **建構時把版本號注入到 `index.html`**
2. **當前頁面啟動後讀取 HTML 中的版本號,作為「當前版本」**
3. **定時重新請求最新的 `index.html`,解析其中的版本號,作為「線上最新版本」**
4. **如果兩者不同,則提示使用者重新整理頁面**

### 完整範例:

<div><div><div></div><span>html</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>meta</span> <span>name</span>=<span>"app-version"</span> <span>content</span>=<span>"20260317-abc123"</span> /></span>

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>function</span> <span>getCurrentVersion</span>(<span></span>) {</span>
<span>  <span>return</span> <span>document</span></span>
<span>    .<span>querySelector</span>(<span>'meta[name="app-version"]'</span>)</span>
<span>    ?.<span>getAttribute</span>(<span>'content'</span>);</span>
<span>}</span>
<span></span>
<span><span>async</span> <span>function</span> <span>fetchLatestVersionFromHtml</span>(<span></span>) {</span>
<span>  <span>const</span> res = <span>await</span> <span>fetch</span>(<span>`/index.html?t=<span>${<span>Date</span>.now()}</span>`</span>, {</span>
<span>    <span>cache</span>: <span>'no-store'</span></span>
<span>  });</span>
<span>  <span>const</span> html = <span>await</span> res.<span>text</span>();</span>
<span></span>
<span>  <span>const</span> match = html.<span>match</span>(</span>
<span>    <span>/<meta></meta></span>
<span>  );</span>
<span></span>
<span>  <span>return</span> match ? match[<span>1</span>] : <span>null</span>;</span>
<span>}</span>
<span></span>
<span><span>// 發現新版本後,友善地提示使用者重新整理</span></span>
<span><span>function</span> <span>showUpdateDialog</span>(<span></span>) {</span>
<span>  <span>const</span> ok = <span>window</span>.<span>confirm</span>(<span>'系統已更新,是否立即重新整理頁面?'</span>);</span>
<span>  <span>if</span> (ok) {</span>
<span>    <span>window</span>.<span>location</span>.<span>reload</span>();</span>
<span>  }</span>
<span>}</span>
<span></span>
<span><span>async</span> <span>function</span> <span>checkVersion</span>(<span></span>) {</span>
<span>  <span>try</span> {</span>
<span>    <span>const</span> currentVersion = <span>getCurrentVersion</span>();</span>
<span>    <span>const</span> latestVersion = <span>await</span> <span>fetchLatestVersionFromHtml</span>();</span>
<span></span>
<span>    <span>if</span> (currentVersion && latestVersion && currentVersion !== latestVersion) {</span>
<span>      <span>showUpdateDialog</span>();</span>
<span>    }</span>
<span>  } <span>catch</span> (err) {</span>
<span>    <span>console</span>.<span>error</span>(<span>'版本檢測失敗:'</span>, err);</span>
<span>  }</span>
<span>}</span>
<span></span>
<span><span>checkVersion</span>();</span>
<span><span>setInterval</span>(checkVersion, <span>5</span> * <span>60</span> * <span>1000</span>);</span>
<span></span>
<span><span>document</span>.<span>addEventListener</span>(<span>'visibilitychange'</span>, <span>() =></span> {</span>
<span>  <span>if</span> (<span>document</span>.<span>visibilityState</span> === <span>'visible'</span>) {</span>
<span>    <span>checkVersion</span>();</span>
<span>  }</span>
<span>});</span>
</span>

---

方案二:捕捉 chunk 載入失敗,作為最終兜底
------------------------

如果說「版本檢測 + 重新整理提示」是在**事前預防**,
那麼「chunk 載入失敗自動恢復」就是最關鍵的**事後兜底**。

這一層非常重要,因為現實中總會遇到這樣的情況:

- 版本檢測還沒來得及執行
- 使用者剛好在檢測間隔內點了選單
- 伺服器剛完成發佈
- 某個懶載入 chunk 已經失效

這時候,問題已經發生了。
如果沒有兜底機制,使用者就會直接看到報錯或白屏。

### 常見錯誤形式

不同建構工具、瀏覽器環境下,報錯資訊可能略有差異,但常見的有:

- `Loading chunk xxx failed`
- `ChunkLoadError`
- `Failed to fetch dynamically imported module`

這類錯誤本質上都可以理解為:

> 動態載入的資源拿不到了。

### 監聽全域錯誤

可以透過以下方式統一攔截:

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>function</span> <span>isChunkLoadError</span>(<span>error</span>) {</span>
<span> <span>const</span> message = error?.<span>message</span> || error?.<span>reason</span>?.<span>message</span> || <span>''</span>;</span>
<span> <span>return</span> (</span>
<span> message.<span>includes</span>(<span>'Loading chunk'</span>) ||</span>
<span> message.<span>includes</span>(<span>'ChunkLoadError'</span>) ||</span>
<span> message.<span>includes</span>(<span>'Failed to fetch dynamically imported module'</span>)</span>
<span> );</span>
<span>}</span>

監聽 error

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>window</span>.<span>addEventListener</span>(<span>'error'</span>, <span>(<span>event</span>) =></span> {</span>
<span>  <span>if</span> (<span>isChunkLoadError</span>(event.<span>error</span> || event)) {</span>
<span>    <span>handleChunkLoadError</span>();</span>
<span>  }</span>
<span>});</span>

監聽 `unhandledrejection`:

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>window</span>.<span>addEventListener</span>(<span>'unhandledrejection'</span>, <span>(<span>event</span>) =></span> {</span>
<span> <span>if</span> (<span>isChunkLoadError</span>(event.<span>reason</span> || event)) {</span>
<span> <span>handleChunkLoadError</span>();</span>
<span> }</span>
<span>});</span>

使用 vite 則不用監聽全域錯誤

vite 官方已經提供預載入錯誤的事件,可以直接使用

image.png

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>// 監聽 vite 預載入錯誤,如果發生錯誤,則重新載入頁面</span></span>
<span><span>window</span>.<span>addEventListener</span>(<span>'vite:preloadError'</span>, <span>() =></span> {</span>
<span>  <span>window</span>.<span>location</span>.<span>reload</span>();</span>
<span>});</span>

### 自動重新整理一次,但一定要避免死循環

如果遇到這類錯誤,可以嘗試自動重新整理頁面一次。
因為重新整理後,瀏覽器會重新請求最新的 `index.html` 和資源入口,大多數情況下問題就能恢復。

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>function</span> <span>handleChunkLoadError</span>(<span></span>) {</span>
<span> <span>const</span> key = <span>'app_chunk_reload_once'</span>;</span>
<span></span>
<span> <span>if</span> (!sessionStorage.<span>getItem</span>(key)) {</span>
<span> sessionStorage.<span>setItem</span>(key, <span>'1'</span>);</span>
<span> <span>alert</span>(<span>'系統資源已更新,正在為您重新整理頁面'</span>);</span>
<span> <span>window</span>.<span>location</span>.<span>reload</span>();</span>
<span> } <span>else</span> {</span>
<span> <span>console</span>.<span>error</span>(<span>'重新整理後仍然失敗,請提示使用者手動重新整理或聯絡管理員'</span>);</span>
<span> }</span>
<span>}</span>

頁面正常載入成功後清除標記:

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span><span>window</span>.<span>addEventListener</span>(<span>'load'</span>, <span>() =></span> {</span>
<span>  sessionStorage.<span>removeItem</span>(<span>'app_chunk_reload_once'</span>);</span>
<span>});</span>

### 為什麼只自動重新整理一次

因為如果失敗原因不是「版本切換」,而是:

- 網路異常
- CDN 故障
- 資源伺服器不可用
- 權限攔截

那麼無限重新整理只會讓問題更嚴重,甚至讓使用者完全無法操作。

所以最佳實踐是:

- 自動重新整理一次嘗試恢復
- 如果仍失敗,再提示使用者手動處理或聯絡支援人員

方案三:快取策略要正確,否則問題會被放大
--------------------

很多時候,問題不是前端程式沒寫好,而是快取策略沒設定好。

一個很典型的原則是:

> 入口檔案要盡快更新,靜態資源要放心快取,版本檔案要即時可讀。

### 1)index.html 不要強快取

`index.html` 是整個 SPA 的入口。
如果它被長時間快取,使用者就可能始終拿不到新的資源入口對映。

建議策略:

- `no-cache`
- 或更嚴格的 `no-store`

例如 Nginx:

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span>location = /index.<span>html</span> {</span>
<span> add_header <span>Cache</span>-<span>Control</span> <span>"no-cache, no-store, must-revalidate"</span>;</span>
<span>}</span>

2)帶 hash 的 js/css 可以強快取

這類資源天然適合長期快取,因為檔名已經包含內容簽名。
只要內容變化,hash 就會變,瀏覽器就會自動拉取新檔案。

例如:

<div><div><div></div><span>js</span></div><div><div> <span>體驗AI代碼助手</span></div><div> <span>代碼解讀</span></div><div>複製代碼</div></div></div>```
<span>location /assets/ {</span>
<span>    add_header <span>Cache</span>-<span>Control</span> <span>"public, max-age=31536000, immutable"</span>;</span>
<span>}</span>


這樣可以顯著提升載入效能。

總結
--

回到最初的問題:

> 前端 SPA 發版後,為什麼使用者停留在舊頁面會導致白屏?又該如何更好地解決?

答案是:

因為 SPA 頁面會長期執行在瀏覽器中,而新版本發佈後靜態資源檔名、資源對映和懶載入 chunk 都可能發生變化。如果使用者仍停留在舊頁面中繼續操作,就很容易在後續資源請求中觸發載入失敗,從而導致頁面報錯甚至白屏。

而更好的解決方式,不是單純依賴「讓使用者手動重新整理」,而是建立一套完整的更新治理方案:

- **版本檢測**:前端主動感知線上是否已更新
- **重新整理提示**:讓使用者在合適時機切換到新版本
- **異常兜底**:chunk 載入失敗時自動重新整理恢復
- **快取優化**:保證入口即時更新、資源合理快取

如果只能先做一步,我最建議先落實的是:

> **捕捉 chunk 載入失敗並自動重新整理一次**

因為它最直接解決「白屏止血」問題。

---

原文出處:https://juejin.cn/post/7617772728414502939

精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬9   ❤️1
424
🥈
我愛JS
📝1   💬8  
51
🥉
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付