fix: 发版后懒加载 chunk 失效白屏——ErrorBoundary 自动刷新 + lazy 重试 #1732

Merged
auto-approve-bot merged 2 commits from fix/chunk-load-white-screen into develop 2026-09-06 13:35:39 +08:00
Owner

背景

所有页面 React Router lazy 动态 import。发版后旧 chunk 文件名被删除,停留在旧标签页的用户点菜单时 import 404(Failed to fetch dynamically imported module / ChunkLoadError),无任何兜底,整页白屏。

改动(apps/web)

1. 全局 ErrorBoundary 捕获 chunk 失效自动恢复

  • utils/chunkLoadError.ts:识别 chunk 加载错误(Vite / webpack / 字符串多形态);sessionStorage 标记最近 10min 内是否已为 chunk 失效自动刷新过(防刷新死循环,标记 TTL 10min 过期后允许再次自愈)
  • components/common/ChunkErrorBoundary.tsx:包裹 RouterProvider
    • chunk 错误首次捕获:打标记 + 自动 window.location.reload()(刷新后 index.html 是 no-cache 的,拿到新 chunk 引用,白屏自愈)
    • 已刷新过仍失败:不再自动刷新,显示「系统已更新,请点击刷新」兜底按钮(手动 hardReload 清标记整页刷新)
    • 其他非 chunk 错误:显示通用「页面出现异常」页,不再整页白屏无反馈

2. 路由 lazy 加载失败重试

  • router/lazyRoute.ts:包一层自动重试(最多 2 次,300/800ms),网络抖动/瞬态失败无感恢复;chunk 404 重试仍失败(旧文件名已不存在)则抛出交给 Boundary;非 chunk 错误立即抛出不浪费重试
  • appRoutes.tsx:28 个路由 lazy 统一替换为 lazyRoute

3. nginx 缓存策略(经核实已满足,未改动)

  • infra/docker/nginx.conf 现状:location = /index.html 已是 no-cache, no-store, must-revalidate(且 try_files ... /index.html 的内部跳转匹配该 location),/assets/* 静态资源 expires 1y + immutable。符合「index.html 每次校验、hash 资源长缓存」要求,无需改动。部署后会实测响应头确认。

测试

  • 新增 16 例(全量 685 passed):
    • chunk 错误识别(Vite TypeError / webpack ChunkLoadError / 字符串 / 普通错误不命中)
    • reload 标记:写入 / TTL 过期 / hardReload 清除
    • Boundary:正常渲染、首次 chunk 错误触发自动刷新 + 提示、二次失败显示手动兜底按钮且不再自动刷新、非 chunk 错误显示通用页
    • lazyRoute:首次成功、重试第 3 次成功、重试 2 次耗尽抛出、非 chunk 错误不重试
  • tsc / eslint / prettier / build 全绿
## 背景 所有页面 React Router lazy 动态 import。发版后旧 chunk 文件名被删除,停留在旧标签页的用户点菜单时 import 404(`Failed to fetch dynamically imported module` / ChunkLoadError),无任何兜底,整页白屏。 ## 改动(apps/web) **1. 全局 ErrorBoundary 捕获 chunk 失效自动恢复** - `utils/chunkLoadError.ts`:识别 chunk 加载错误(Vite / webpack / 字符串多形态);sessionStorage 标记最近 10min 内是否已为 chunk 失效自动刷新过(防刷新死循环,标记 TTL 10min 过期后允许再次自愈) - `components/common/ChunkErrorBoundary.tsx`:包裹 RouterProvider - chunk 错误**首次**捕获:打标记 + 自动 `window.location.reload()`(刷新后 index.html 是 no-cache 的,拿到新 chunk 引用,白屏自愈) - **已刷新过仍失败**:不再自动刷新,显示「系统已更新,请点击刷新」兜底按钮(手动 hardReload 清标记整页刷新) - 其他非 chunk 错误:显示通用「页面出现异常」页,不再整页白屏无反馈 **2. 路由 lazy 加载失败重试** - `router/lazyRoute.ts`:包一层自动重试(最多 2 次,300/800ms),网络抖动/瞬态失败无感恢复;chunk 404 重试仍失败(旧文件名已不存在)则抛出交给 Boundary;非 chunk 错误立即抛出不浪费重试 - `appRoutes.tsx`:28 个路由 lazy 统一替换为 `lazyRoute` **3. nginx 缓存策略(经核实已满足,未改动)** - `infra/docker/nginx.conf` 现状:`location = /index.html` 已是 `no-cache, no-store, must-revalidate`(且 `try_files ... /index.html` 的内部跳转匹配该 location),`/assets/*` 静态资源 `expires 1y + immutable`。符合「index.html 每次校验、hash 资源长缓存」要求,无需改动。部署后会实测响应头确认。 ## 测试 - 新增 16 例(全量 685 passed): - chunk 错误识别(Vite TypeError / webpack ChunkLoadError / 字符串 / 普通错误不命中) - reload 标记:写入 / TTL 过期 / hardReload 清除 - Boundary:正常渲染、首次 chunk 错误触发自动刷新 + 提示、二次失败显示手动兜底按钮且不再自动刷新、非 chunk 错误显示通用页 - lazyRoute:首次成功、重试第 3 次成功、重试 2 次耗尽抛出、非 chunk 错误不重试 - tsc / eslint / prettier / build 全绿
xiaoxia added 1 commit 2026-09-06 13:21:45 +08:00
fix: 发版后懒加载 chunk 失效白屏——ErrorBoundary 自动刷新 + lazy 重试
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m43s
AI Code Review / AI Code Review (pull_request) Failing after 1m49s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m58s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m8s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m27s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
c894163446
根因:所有页面 React Router lazy 动态 import,发版后旧 chunk 文件名删除,
停留旧标签页的用户点菜单时 import 404(Failed to fetch dynamically
imported module / ChunkLoadError),无兜底导致整页白屏。

改动:
- utils/chunkLoadError.ts:识别 chunk 加载错误(Vite/webpack/字符串多形态);
  sessionStorage 标记最近 10min 内是否已为 chunk 失效自动刷新过(防死循环)
- components/common/ChunkErrorBoundary.tsx:全局错误边界包裹 RouterProvider;
  chunk 错误首次捕获自动整页刷新(刷新后 no-cache 的 index.html 拿到新 chunk
  引用自愈),已刷新过仍失败则显示「系统已更新,请点击刷新」兜底按钮;
  其他错误显示通用异常页,不再整页白屏
- router/lazyRoute.ts:路由 lazy 包一层自动重试(最多 2 次,300/800ms),
  网络抖动无感恢复;chunk 404 重试耗尽抛出交给 Boundary;非 chunk 错误立即抛
- appRoutes.tsx:28 个路由 lazy 统一替换为 lazyRoute
- nginx 缓存策略经核实已满足要求:index.html no-cache、/assets/* immutable 1y

单测 16 例:chunk 错误识别多形态、reload 标记生命周期、Boundary 首次自动刷新/
二次手动兜底/非 chunk 通用页、lazyRoute 重试成功/耗尽抛出/非 chunk 不重试。

🚀 预览环境已部署

项目 详情
PR号 #1732
预览链接 https://pr-1732.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1732 | | 预览链接 | [https://pr-1732.preview.xiaoxiajianji.com](https://pr-1732.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
auto-approve-bot approved these changes 2026-09-06 13:23:53 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
auto-approve-bot approved these changes 2026-09-06 13:23:53 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
xiaoxia added 1 commit 2026-09-06 13:29:02 +08:00
fix: 按 AI Review 意见加固——非chunk错误跳首页防死循环 + storage异常防护
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m43s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m3s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m14s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m31s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m58s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 5m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m7s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 18s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 10m13s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 10m15s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 10m21s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
320aa89751
- 手动兜底由 hardReload(刷新当前URL) 改为 goHomeRecover(跳首页整页导航):
  chunk 失效时脱离旧 chunk 引用;业务崩溃时绕开报错路由,避免刷新-再崩死循环
- chunkLoadError 的 sessionStorage 访问全部包 try-catch(Safari 无痕/禁用
  Cookie 时访问存储会抛异常,错误边界本身不能因此崩溃),降级为无标记
- lazyRoute 增加 mod.default 校验,缺导出时抛明确错误
Collaborator

【阻塞级判定】

  • 是否存在阻塞级问题:是
  • 阻塞级问题数量:1 个

📊 审查概览

  • 整体评价:需修改
  • 建议级问题数量:1 个

🔴 阻塞级问题(必须修复)

  1. [apps/web/src/utils/chunkLoadError.ts: 32-46, 74-77] sessionStorage 不可用会导致无限刷新循环
    • 问题类型:逻辑bug
    • 问题描述:safeStorage.setItem 在捕获异常时会静默失败(不抛错),而 reloadForChunkError 无论 setItem 是否成功都会执行 window.location.reload()。在 Safari 无痕模式或禁用 Cookie/Storage 的严格环境下,sessionStorage 操作会抛出异常。此时 getChunkReloadedAt 因读取失败返回 nullChunkErrorBoundary 判断为未刷新过,于是调用 reloadForChunkError。由于无法写入标记,页面刷新后再次进入相同逻辑,导致浏览器陷入“检测到更新 -> 刷新 -> 检测到更新”的死循环,直到浏览器拦截。
    • 修改建议:建议引入 URL 参数(如 ?chunk_reload_ts=xxx)作为降级标记,或者修改 reloadForChunkError 逻辑,仅在确认标记写入成功后才刷新;若写入失败,应视为无法自动恢复,直接抛出错误或返回状态让 ErrorBoundary 展示手动恢复界面。

💡 改进建议(不阻塞合并)

  1. [apps/web/src/utils/chunkLoadError.ts: 82] 子路径部署兼容性
    • 具体内容:goHomeRecover 函数中使用 window.location.href = "/" 强制跳转根路径。如果应用部署在子路径(如 example.com/my-app),此跳转会将用户带离应用域或导致 404。建议结合 window.location.origin 和应用的 basename 配置,或者使用 React Router 的 navigate(如果可注入)进行相对路径跳转。

良好实践

  • 使用 safeStorage 封装了 sessionStorage 操作,考虑了异常捕获的意识(虽然逻辑需完善)。
  • lazyRoute 实现了自动重试机制,能有效区分网络抖动和真正的版本更新导致的 Chunk 失效。
  • 测试覆盖了 Storage 不可用场景(尽管未完全验证死循环逻辑)以及多种错误类型,单元测试较为完善。
  • 代码注释清晰,详细阐述了设计意图和自愈流程。

格式检查通过 | 逻辑审查需修改 | ⚠️ 建议关注性能


🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 05:34:53 | 模型:

### 【阻塞级判定】 - 是否存在阻塞级问题:是 - 阻塞级问题数量:1 个 ### 📊 审查概览 - 整体评价:需修改 - 建议级问题数量:1 个 ### 🔴 阻塞级问题(必须修复) 1. **[apps/web/src/utils/chunkLoadError.ts: 32-46, 74-77] sessionStorage 不可用会导致无限刷新循环** - 问题类型:逻辑bug - 问题描述:`safeStorage.setItem` 在捕获异常时会静默失败(不抛错),而 `reloadForChunkError` 无论 `setItem` 是否成功都会执行 `window.location.reload()`。在 Safari 无痕模式或禁用 Cookie/Storage 的严格环境下,`sessionStorage` 操作会抛出异常。此时 `getChunkReloadedAt` 因读取失败返回 `null`,`ChunkErrorBoundary` 判断为未刷新过,于是调用 `reloadForChunkError`。由于无法写入标记,页面刷新后再次进入相同逻辑,导致浏览器陷入“检测到更新 -> 刷新 -> 检测到更新”的死循环,直到浏览器拦截。 - 修改建议:建议引入 URL 参数(如 `?chunk_reload_ts=xxx`)作为降级标记,或者修改 `reloadForChunkError` 逻辑,仅在确认标记写入成功后才刷新;若写入失败,应视为无法自动恢复,直接抛出错误或返回状态让 ErrorBoundary 展示手动恢复界面。 ### 💡 改进建议(不阻塞合并) 1. **[apps/web/src/utils/chunkLoadError.ts: 82] 子路径部署兼容性** - 具体内容:`goHomeRecover` 函数中使用 `window.location.href = "/"` 强制跳转根路径。如果应用部署在子路径(如 `example.com/my-app`),此跳转会将用户带离应用域或导致 404。建议结合 `window.location.origin` 和应用的 `basename` 配置,或者使用 React Router 的 `navigate`(如果可注入)进行相对路径跳转。 ### ✅ 良好实践 - 使用 `safeStorage` 封装了 sessionStorage 操作,考虑了异常捕获的意识(虽然逻辑需完善)。 - `lazyRoute` 实现了自动重试机制,能有效区分网络抖动和真正的版本更新导致的 Chunk 失效。 - 测试覆盖了 Storage 不可用场景(尽管未完全验证死循环逻辑)以及多种错误类型,单元测试较为完善。 - 代码注释清晰,详细阐述了设计意图和自愈流程。 --- ✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 05:34:53 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
auto-approve-bot merged commit 9a289e1e1f into develop 2026-09-06 13:35:39 +08:00
auto-approve-bot deleted branch fix/chunk-load-white-screen 2026-09-06 13:35:41 +08:00

🗑️ 预览环境已清理

PR #1732 已关闭或合并,对应的预览环境已被清理。

如有需要,可以重新打开 PR 来重新生成预览环境。

🗑️ **预览环境已清理** PR #1732 已关闭或合并,对应的预览环境已被清理。 > 如有需要,可以重新打开 PR 来重新生成预览环境。
Sign in to join this conversation.