diff --git a/apps/web/src/test/utils/chunkLoadError.test.ts b/apps/web/src/test/utils/chunkLoadError.test.ts index b7a5b9997..83637c2fe 100644 --- a/apps/web/src/test/utils/chunkLoadError.test.ts +++ b/apps/web/src/test/utils/chunkLoadError.test.ts @@ -77,4 +77,25 @@ describe("reload 标记", () => { spy.mockRestore() setSpy.mockRestore() }) + + it("URL 带 chunkreload 参数时视为已刷新过(storage 不可用的降级标记)", () => { + window.history.replaceState({}, "", "/app/assets?chunkreload=1") + expect(getChunkReloadedAt()).not.toBeNull() + window.history.replaceState({}, "", "/") + }) + + it("storage 完全不可用时,reloadForChunkError 走 URL 标记跳转(防无痕死循环)", () => { + const setSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("Storage disabled") + }) + const getSpy = vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { + throw new Error("Storage disabled") + }) + window.history.replaceState({}, "", "/app/assets") + // reloadForChunkError 应走 location.replace 带上 ?chunkreload=1(jsdom 不会真导航) + expect(() => reloadForChunkError()).not.toThrow() + setSpy.mockRestore() + getSpy.mockRestore() + window.history.replaceState({}, "", "/") + }) }) diff --git a/apps/web/src/utils/chunkLoadError.ts b/apps/web/src/utils/chunkLoadError.ts index 7dce61a66..280d4b4b7 100644 --- a/apps/web/src/utils/chunkLoadError.ts +++ b/apps/web/src/utils/chunkLoadError.ts @@ -57,8 +57,26 @@ export const isChunkLoadError = (error: unknown): boolean => { ) } -/** 读取上次自动刷新时间戳;过期或不存在返回 null */ +/** URL 降级标记参数:storage 不可用(无痕/禁 Cookie)时靠它在刷新后保留"已刷新"状态 */ +const RELOAD_QUERY_KEY = "chunkreload" + +/** 判断当前 URL 是否已带"为 chunk 失效刷新过"标记 */ +const urlHasReloadFlag = (): boolean => { + try { + return new URLSearchParams(window.location.search).has(RELOAD_QUERY_KEY) + } catch { + return false + } +} + +/** + * 读取"已自动刷新过"状态:storage 与 URL 参数双重判定。 + * storage 在 Safari 无痕等环境可能完全不可用,此时刷新后标记丢失会陷入 + * "检测到 chunk 失效 → 刷新 → 再检测 → 再刷新"死循环;URL 参数刷新后仍在, + * 作为降级标记兜住这种环境。 + */ export const getChunkReloadedAt = (): number | null => { + if (urlHasReloadFlag()) return Date.now() const raw = safeStorage.getItem(RELOAD_FLAG_KEY) if (!raw) return null const ts = Number(raw) @@ -70,6 +88,15 @@ export const getChunkReloadedAt = (): number | null => { /** 标记"已为 chunk 失效自动刷新过",然后刷新页面 */ export const reloadForChunkError = (): void => { safeStorage.setItem(RELOAD_FLAG_KEY, String(Date.now())) + // 同时在 URL 上带标记:storage 写不进(无痕模式)时刷新后仍能识别已刷新过 + try { + const url = new URL(window.location.href) + url.searchParams.set(RELOAD_QUERY_KEY, "1") + window.location.replace(url.toString()) + return + } catch { + /* URL 构造失败则退回普通刷新 */ + } window.location.reload() }