Lightdash Chunk Error Handler:Vite 部署后过期 JS Chunk 的自动恢复机制

Lightdash Chunk Error Handler:Vite 部署后过期 JS Chunk 的自动恢复机制 Lightdash Chunk Error HandlerVite 部署后过期 JS Chunk 的自动恢复机制【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash导读Lightdash 前端基于 Vite 构建每次发布都会生成带内容哈希的文件名的 JS chunk当部署新版本后仍停留在旧页面的用户浏览器会因为引用已被删除的旧 chunk 地址而触发Failed to fetch dynamically imported module等动态导入错误。本文以 packages/frontend/src/features/chunkErrorHandler/CLAUDE.md 为主干结合 chunkErrorHandler.ts 及其测试、集成点源码完整讲解 chunkErrorHandler 模块的设计原理、API 用法与在 ErrorBoundary、Sentry、路由懒加载等场景中的落地方式。读完你既能理解这类部署后白屏问题的成因也能在自己维护的 Vite React 应用中复刻这套自动刷新一次、失败后再提示手动刷新的恢复方案。一、问题背景Vite 内容哈希 chunk 与部署后的陈旧引用现代前端构建工具如 Vite、Webpack在打包时会给每个异步模块生成带内容哈希的独立 chunk 文件例如ExplorePanel-abc123.js。Lightdash 的 App.tsx 与路由文件大量使用 React 的lazy()动态导入配合 Vite 的代码分割将不同页面/功能拆成多个 chunk。当 Lightdash 部署新版本时会依次发生Vite 生成带有内容哈希文件名的新 JS chunk例如ExplorePanel-abc123.js旧 chunk 从服务器上被删除尚未刷新页面、仍持有缓存 HTML 的用户其 HTML 里引用的仍是旧 chunk 的 URL当React.lazy()尝试加载这个已不存在的旧 chunk 时浏览器返回 404进而抛出动态导入错误。结果就是用户明明网络正常却突然看到页面报错甚至白屏——这类问题在持续部署 长生命周期 SPA的产品中非常典型。chunkErrorHandler 模块正是针对这一场景检测这类 chunk 加载失败错误并自动刷新页面以获取携带正确 chunk 引用的新 HTML。从源码实现看Vite 预加载失败和动态导入失败还可能表现为多种不同的报错文本chunkErrorHandler.ts中维护了一份错误消息清单见下文错误识别小节涵盖vite:preloadError事件与未捕获的 Promise rejection 两条捕获路径。二、核心 API 全景模块的公开导出统一由 index.ts 汇总export { hasRecentChunkReload, isChunkLoadError, isChunkLoadErrorObject, RouteChunkLoadError, triggerChunkErrorReload, } from ./chunkErrorHandler; export { loadLazyRouteDefault } from ./loadLazyRouteDefault;1.isChunkLoadError(message: string): boolean纯字符串判断用于 Error 对象的 message 或直接拿到的错误文本如 Web Worker 返回的错误信息。实现上对输入消息做toLowerCase()归一化后与内置的错误消息清单做大小写不敏感的包含匹配const CHUNK_ERROR_MESSAGES [ Failed to fetch dynamically imported module, error loading dynamically imported module, Importing a module script failed, Failed to load module script, Unable to preload CSS, // Route lazy imports can surface a failed preload as destructuring undefined. Cannot destructure property default of (intermediate value) as it is undefined, ];其中最后一条比较隐蔽路由懒加载的失败可能以解构default得到undefined的形式冒出来因此被单独列入。对应测试见 chunkErrorHandler.test.ts 中的detects dynamic import and preload failures用例它验证了TypeError: Failed to fetch dynamically imported module、Importing a module script failed.、Unable to preload CSS for /assets/app.css等均能被识别同时Network request failed、Something went wrong这类无关错误不会被误判。2.isChunkLoadErrorObject(error: unknown): boolean针对 Error 对象的检测入口用于错误边界ErrorBoundary等拿到unknown类型错误的场景。逻辑分三层若错误是RouteChunkLoadError实例直接判定为 chunk 错误若是普通Error实例则用其message走isChunkLoadError字符串匹配其他类型非 Error一律返回false。3.RouteChunkLoadError一个自定义 Error 子类携带routeModule字段记录具体是哪个路由模块加载失败export class RouteChunkLoadError extends Error { routeModule: string; constructor(routeModule: string) { super(Route chunk failed to load: ${routeModule}); this.name RouteChunkLoadError; this.routeModule routeModule; } }它在两条链路中发挥价值一是路由懒加载包装器loadLazyRouteDefault在导入结果缺少default导出时抛出二是 Sentry 集成用它给错误打上route.module标签和独立指纹方便在监控后台归类排查见下文 Sentry 小节。4.hasRecentChunkReload(): boolean与triggerChunkErrorReload(): void这一对函数构成60 秒冷却机制的读写两端是防止无限刷新循环的关键triggerChunkErrorReload()先把当前时间戳写入sessionStorage的lightdash-chunk-error-reload键然后调用window.location.reload()刷新页面hasRecentChunkReload()读取该键若不存在返回false若时间戳距今超过RELOAD_COOLDOWN_MS 60_00060 秒则清除键并返回false60 秒内返回true。选择sessionStorage而非localStorage是有意为之刷新后旧标签页会话仍持有标记可防止刷新后立即再次失败 → 再次自动刷新的死循环而不同标签页互不影响。代码还对sessionStorage可能抛异常隐私模式或存储被禁用做了 try/catch 兜底——读取异常时返回true保守地跳过自动刷新、展示手动刷新 UI写入异常时仍照常 reload最坏情况用户看到手动刷新界面。5.installChunkLoadErrorHandler(): void全局事件监听安装函数在 App.tsx 顶层模块加载时调用installChunkLoadErrorHandler()。它通过模块级布尔量isChunkLoadErrorHandlerInstalled保证只安装一次且typeof window undefined时直接跳过兼容 SSR/测试环境。安装后监听两类事件vite:preloadErrorVite 预加载失败事件若允许刷新则preventDefault()阻止默认行为并触发自动刷新unhandledrejection未捕获的 Promise rejection先经isChunkLoadErrorObject判定命中且允许刷新则preventDefault()后自动刷新。两者共用内部辅助函数reloadOnceForChunkError()若 60 秒内已经刷新过则返回false不再刷新否则调用triggerChunkErrorReload()并返回true。6.loadLazyRouteDefault(routeModule, importer)路由懒加载的强化包装器见 loadLazyRouteDefault.ts。它包一层importer()即() import(./pages/Home)形式的动态导入函数在导入成功但模块缺少default导出时抛出带路由名的RouteChunkLoadError——将加载成功但结构不对也统一转化为可被上层错误边界识别的 chunk 错误。其行为由 loadLazyRouteDefault.test.ts 验证正常情况返回 default 组件模块解析为undefined时抛出RouteChunkLoadError并携带正确的message与routeModule。该函数被大量路由文件采用例如 Routes.tsx 中超过五十处路由的懒加载Register、Login、SavedQueries、Explorer、Dashboard等、AuthRoutes.tsx 的认证相关路由以及 CommercialRoutes.tsx 中企业版功能Embed 嵌入、AI Agent、Slack 回调等的懒加载。三、已集成的三条使用链路CLAUDE.md 明确指出该模块已经集成在三个位置通常无需直接调用除非要在新的位置处理 chunk 错误。1. 应用级错误边界ErrorBoundary.tsxErrorBoundary.tsx 是基于Sentry.ErrorBoundary的通用错误边界。其内部ErrorFallback组件是 chunk 自动恢复的主战场先调用isChunkLoadErrorObject(error)判断错误类型若是 chunk 错误且!hasRecentChunkReload()60 秒内未刷新过则调用triggerChunkErrorReload()并渲染null——页面即将刷新无需展示任何 UI若自动刷新已经尝试过冷却期内再次失败则渲染ChunkErrorFallback手动刷新界面提示Application update required其他普通错误走GeneralErrorFallback展示错误详情与 Sentry event ID 供用户联系支持。2. 路由级错误边界ChunkErrorRouteBoundary.tsx一个容易被忽略的细节是react-router 的路由级lazychunk 加载失败会被 react-router 自己捕获既不会进入 React 错误边界也不会触发window的unhandledrejection监听。为此 ChunkErrorRouteBoundary.tsx 作为路由的errorElement见 App.tsx单独处理这一场景使用useRouteError()拿到路由错误走与ErrorBoundary完全相同的判定逻辑自动刷新一次 → 失败后展示ChunkErrorFallback。该组件的测试 ChunkErrorRouteBoundary.test.tsx 用 mock 分别验证了三条分支无近期刷新时恰好触发一次自动刷新且不渲染 fallback已刷新过则展示 chunk fallback 且不再触发刷新非 chunk 错误则调用Sentry.captureException并展示 general fallback。3. 透视表 Worker 错误SimpleTable/index.tsxSimpleTable/index.tsx 展示了不做自动刷新的第三种用法当透视表pivot table的 Web Worker 数据获取错误经isChunkLoadError(pivotTableData.error)判定为 chunk 错误时直接渲染一个包含Refresh page按钮的SuboptimalState提示点击按钮调用triggerChunkErrorReload()手动刷新。原因在于 Worker 错误通常由isChunkLoadError这类字符串检测捕获场景相对可控且错误发生在局部组件而非应用根部自动刷新反而可能打断用户正在进行的操作。四、Sentry 集成自动刷新成功前不打扰监控hooks/thirdPartyServices/useSentry.ts 在beforeSend钩子中实现了只有自动刷新失败才上报的过滤策略// For chunk load errors, only send to Sentry if auto-reload already failed if ( isChunkLoadErrorObject(error) !hasRecentChunkReload() ) { return null; }即若错误是 chunk 加载错误且 60 秒内没有发生过自动刷新说明刷新前就被捕获到直接丢弃不上报——因为马上会触发自动刷新这类错误属于预期内、可自愈的事件上报只会污染错误指标只有自动刷新已经失败冷却期内再次报错才放行让团队真正关注到用户无法自愈的情况。针对RouteChunkLoadErrorbeforeSend还会为其附加route.module标签记录具体失败路由并设置独立指纹[route-chunk-load-error]使这类错误在 Sentry 中能够聚合统计、单独追踪。此外代码还顺带过滤了完全来自第三方代码的 SyntaxError典型成因是网络问题、浏览器插件或 CDN 分发损坏的 bundle避免无效告警刷屏。五、错误提示 UI从自动恢复到人工兜底当自动刷新最终失败60 秒冷却期内重复报错用户会看到 ErrorFallbacks.tsx 中定义的ChunkErrorFallback组件标题为Application update required说明Lightdash 有新版本可用请刷新浏览器加载最新版本并附带提示——若刷新后问题依旧请尝试清除浏览器缓存或以无痕窗口打开。UI 上提供Refresh page按钮点击再次调用triggerChunkErrorReload()。而GeneralErrorFallback则面向普通错误展示 Sentry event ID 与错误堆栈信息供用户反馈给支持团队。两条 UI 路径都通过 Mantine 的Flex容器承载并带有ERROR_BOUNDARY_ID与data-error-message等属性便于端到端测试与自动化排查时定位元素。六、在新的位置使用该模块CLAUDE.md 给出了在任意错误边界 fallback 中的推荐用法这也是该模块面向开发者的核心接口import { isChunkLoadErrorObject, hasRecentChunkReload, triggerChunkErrorReload, } from ../chunkErrorHandler; // In an error boundary fallback: if (isChunkLoadErrorObject(error)) { if (!hasRecentChunkReload()) { triggerChunkErrorReload(); // Auto-reload once return null; } // Show manual refresh UI (auto-reload already failed) }结合前文可以总结出在新位置接入时的三个关键决策点用什么识别错误拿到字符串如 Worker 的 error message用isChunkLoadError拿到unknown的 Error 用isChunkLoadErrorObject不要对非 Error 值做假设是否自动刷新应用级、路由级错误适合自动刷新一次 失败后手动刷新局部组件如透视表 Worker建议只提供手动刷新按钮避免打断用户操作如何防循环永远通过hasRecentChunkReload()判断冷却状态把是否刷新的决定交给模块统一管理不要在业务代码里自行维护刷新标记。模块的单元测试 chunkErrorHandler.test.ts 覆盖了错误识别与RouteChunkLoadError判定可作为接入新位置时的回归基线。七、小结chunkErrorHandler 通过事件监听 错误特征匹配 sessionStorage 冷却 分级降级自动刷新 → 手动刷新 UI的组合为 Lightdash 解决了 Vite 部署后陈旧 chunk 导致的白屏与报错问题。其设计要点值得借鉴错误识别使用统一的消息清单而非分散硬编码防循环依赖跨刷新存活的sessionStorage与 60 秒冷却监控集成遵循可自愈的事件不上报原则UI 兜底在自动恢复失效时仍给出清晰的人工操作指引。整套机制从 chunkErrorHandler.ts 的核心实现到 ErrorBoundary.tsx、ChunkErrorRouteBoundary.tsx、useSentry.ts 与 SimpleTable/index.tsx 的四处集成再到配套的 单元测试构成了一条完整、可复现、可测试的部署后平滑恢复实践链路。【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考