===============================
場景
--
在單頁應用(SPA)專案中,有一個問題非常常見,但又經常被低估:系統明明已經發佈了新版本,部分使用者卻依然停留在舊頁面中繼續操作。
大多數時候,這種狀態並不會立刻出問題,所以團隊往往不太在意。但一旦使用者繼續進行路由跳轉、訪問懶載入頁面,或者觸發某些依賴新資源的操作,就可能出現下面這些現象:
Loading chunk failedFailed to fetch dynamically imported module這類問題在線上系統裡並不少見,尤其是管理後台、教學平台、營運平台這類使用者會長時間掛著頁面不重新整理的 SPA 應用。 如果處理不好,不僅影響使用者體驗,還會帶來很多「難定位、難重現」的線上問題。
這篇文章,我想系統說清楚三件事:
這個問題的完整鏈路可以概括為:
<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 官方已經提供預載入錯誤的事件,可以直接使用

<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>
這類資源天然適合長期快取,因為檔名已經包含內容簽名。
只要內容變化,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