/** * 发版后旧标签页懒加载 chunk 失效(白屏)的识别与恢复工具。 * * 背景:页面 React Router 的 lazy 动态 import,发版后旧 chunk 文件名被删除, * 停留在旧标签页的用户点菜单时 import 404,抛出 * "Failed to fetch dynamically imported module"(Vite)/ ChunkLoadError, * 不捕获就是整页白屏。 */ /** sessionStorage 标记:最近已经为 chunk 失效自动刷新过一次(带时间戳,10min 有效) */ const RELOAD_FLAG_KEY = "chunk_error_reloaded_at" /** 标记有效期:超过后允许再次自动刷新,避免用户手动正常刷新后标记永久残留 */ const RELOAD_FLAG_TTL_MS = 10 * 60 * 1000 /** * Storage 在 Safari 无痕模式 / 禁用 Cookie 的浏览器 / 严格 iframe 策略下 * 访问可能抛异常;此处统一容错,拿不到存储就降级为"无标记",绝不能让 * 错误边界本身因读存储而崩溃。 */ const safeStorage = { getItem: (key: string): string | null => { try { return sessionStorage.getItem(key) } catch { return null } }, setItem: (key: string, value: string): void => { try { sessionStorage.setItem(key, value) } catch { /* 存储不可用时静默降级:仅丢失"已刷新"标记,不影响恢复动作 */ } }, removeItem: (key: string): void => { try { sessionStorage.removeItem(key) } catch { /* ignore */ } }, } /** 判断错误是否为懒加载 chunk 加载失败(发版 404 / 网络中断 / 动态 import 失败) */ export const isChunkLoadError = (error: unknown): boolean => { if (!error) return false // Vite: Failed to fetch dynamically imported module: /assets/xxx-yyy.js // Webpack: ChunkLoadError: Loading chunk xxx failed. const needle = error instanceof Error ? `${error.name} ${error.message}` : typeof error === "string" ? error : "" return /failed to fetch dynamically imported module|chunkloaderror|loading chunk \d+ failed|error loading dynamically imported module|importing a module script failed/i.test( needle, ) } /** 读取上次自动刷新时间戳;过期或不存在返回 null */ export const getChunkReloadedAt = (): number | null => { const raw = safeStorage.getItem(RELOAD_FLAG_KEY) if (!raw) return null const ts = Number(raw) if (!Number.isFinite(ts)) return null if (Date.now() - ts > RELOAD_FLAG_TTL_MS) return null return ts } /** 标记"已为 chunk 失效自动刷新过",然后刷新页面 */ export const reloadForChunkError = (): void => { safeStorage.setItem(RELOAD_FLAG_KEY, String(Date.now())) window.location.reload() } /** * 硬恢复:清掉标记后回到首页(整页导航,不是当前 URL 刷新)。 * - chunk 失效兜底:回到首页会拉取最新 index.html,彻底脱离旧 chunk 引用 * - 非 chunk 的页面级崩溃:跳首页能绕开当前报错路由,避免"刷新-再崩"死循环 */ export const goHomeRecover = (): void => { safeStorage.removeItem(RELOAD_FLAG_KEY) window.location.href = "/" }