1. 现象与表象
在生产环境通过浏览器访问类似 http://<host>/scs/warehouse 的地址时,页面间歇性出现整页空白(俗称「白屏」)。开发者工具控制台常见两类信号:
Failed to load module script:期望加载 JavaScript 模块,响应Content-Type却是text/html;后续伴随Failed to fetch dynamically imported module。413 Request Entity Too Large:请求/api/attachments/upload(或同源前缀下的上传接口)被拒。
二者可同时出现,但机理不同:前者关系到静态资源与缓存一致性,后者关系到上传体积上限。下文分开说明。
2. 白屏的技术链路(从请求到崩溃)
2.1 现代前端构建与哈希文件名
使用 Vite、Webpack 等工具打包时,会对 JS/CSS 等资源生成带内容哈希的文件名,例如 assets/index-D7XCgUj3.js。入口 HTML(如 index.html)里通过 <script type="module"> 或运行时注入的路径引用这些文件。路由级别的代码常采用 动态 import()(在 React 中常配合 React.lazy),浏览器会按需再拉取对应 chunk。
因此,一次完整可用的访问需要同时满足:当前页面所依据的入口 HTML 与其引用的各 chunk 文件在服务器上版本一致、路径可解析。
2.2 子路径与 Nginx 的典型配置
将应用挂在 /scs/ 等子路径时,常见 Nginx 模式是:静态根目录指向构建产物 dist/,对未匹配到真实文件的 URI 做 SPA 回退(try_files 落到 index.html),以便客户端路由能处理深链接。
该模式对业务路径是合理的,但对不存在的静态资源会产生副作用:若某次请求针对的是 .../assets/index-旧哈希.js,而服务器上已没有该文件(新发布已删除旧 chunk),try_files 往往仍会落到 index.html。
2.3 为何控制台报「MIME 类型是 text/html」
浏览器对 <script type="module"> 与动态 import 的脚本执行 严格 MIME 校验:响应体必须是 JavaScript(或 WASM),且 MIME 需匹配。
当缺失的 chunk URL 被错误地返回 index.html 时:
- 响应体实质是 HTML 文档;
Content-Type通常为text/html;
此时引擎拒绝将该响应当作 ES Module 执行,于是出现 Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html",动态 import 被拒绝,依赖该 chunk 的视图无法挂载,用户看到的就是白屏或长期停留在加载态。
简言之:不是「JS 写错了」,而是「拿 JS 的地址却收到了整页 HTML」。
2.4 「偶发」从何而来:缓存与发布时间窗口
该问题常表现为偶发,典型场景包括:
| 因素 | 说明 |
|---|---|
| 浏览器缓存 | 用户本地仍保留旧版 index.html(若缓存策略偏激进),其中引用的仍是旧哈希 chunk;服务器已发布新版本,旧文件已删除。 |
| CDN / 边缘缓存 | 入口文件与各 chunk 缓存策略不一致时,会出现「新 HTML + 旧 asset」或相反,造成短时间不一致。 |
| 发布过程非原子 | 若先替换入口、后替换 assets,或相反,会存在极短时间内路径与磁盘不一致。 |
因此,同一套代码在不同用户、不同时刻、是否硬刷新上表现不同,呈现为「偶现」。
2.5 与动态路由(lazy)的关系
使用 React.lazy + Suspense 时,首屏可能正常,进入某个路由后才去加载对应 chunk。若该 chunk 请求失败(同上,拿到 HTML),错误发生在导航之后,用户会感觉是「点进某页才白屏」,控制台则指向具体失败的 assets/xxx.js。
3. 413:上传体积问题(独立链路)
413 Request Entity Too Large 表示 HTTP 请求体超过某一层的限制。常见位置包括:
- 反向代理(如 Nginx):
client_max_body_size; - 应用服务器 / 框架:对 body 大小的默认上限。
这与静态资源 MIME 问题无因果关系:即便上传失败多次刷控制台,也不应以此推断「白屏由 413 直接导致」。但若同一页面既有附件上传又有路由懒加载,两类错误会同时出现在控制台,排查时需分开归因。
4. 治理思路分层
4.1 基础设施与发布(根治向)
- 入口 HTML 缓存策略:
index.html宜用Cache-Control: no-cache或极短max-age,避免长期持有过期入口。 - 静态资源:可对带哈希的
assets/使用较长缓存(文件名变则 URL 变)。 - 发布原子性:整包替换或使用软链/目录切换,避免新旧混用窗口。
- Nginx(可选):对
/assets/等路径不要对缺失文件回退到index.html,使错误表现为 404,便于监控与区分问题。
以上从根上降低「HTML 冒充 JS」的概率;若团队阶段性不能改 Nginx,则需在前端与流程上加强弹性与可恢复性。
4.2 纯前端可做的增强(韧性向)
在无法立刻调整网关或缓存策略时,前端仍可显著改善体验:
- Chunk 加载失败后的有限次整页刷新
若捕获到典型错误信息(如Failed to fetch dynamically imported module、ChunkLoadError等),可视为疑似部署与缓存不一致,在同一浏览器会话内触发 一次location.reload()。多数情况下,重新请求入口 HTML 能拿到新版本引用,后续 chunk 路径与磁盘一致,页面即可恢复。
注意:须配合「只重试一次」之类的标记(如sessionStorage),避免资源永久缺失时形成死循环刷新。 - 错误边界(Error Boundary)
React.lazy在加载失败时会抛错;Suspense不捕获渲染期错误。在外层挂 Error Boundary,可在重试仍失败时展示明确文案(建议硬刷新、清理缓存)与手动重载按钮,避免无限白屏与「无任何反馈」。 - 对 413 的接口层提示
在 HTTP 客户端拦截器中识别413,抛出或展示业务可读的提示(如建议缩小文件、联系管理员调限额),减少「只有网络面板报红、用户不知如何处理」的情况。
上述手段不能抬升服务器上传上限,也不能保证在极端错误配置下仍能加载到正确 JS;它们的目标是:缩短暂不可用时间、降低误判成本、在失败可感知时可恢复。
5. 小结
- 白屏主因:旧入口或错误路由仍指向已不存在的哈希 chunk;缺失请求被 SPA 规则用
index.html顶替,触发 MIME 与动态 import 失败。 - 偶发原因:缓存、CDN、发布窗口与懒加载时机叠加。
- 413 主因:上传体积超过代理或应用限制,需与静态资源问题分开排查。
- 前端方案:在基础设施不变的前提下,通过 chunk 失败检测 + 单次刷新、错误边界 与 413 友好提示 提升可用性与可诊断性;长期仍建议从 缓存策略与发布流程 上收敛问题根源。
若你希望将本文档落到仓库(例如 docs/)或改成对内运维手册口吻,可以说明存放路径与受众(开发 / 运维),我可以按该格式再压缩或增补检查清单。