1. 现象与触发场景 (Symptom & Scenario)

使用现代前端构建工具(如 Vite、Rollup、Webpack 5)打包时,针对 Web Worker、ES Module 动态导入或第三方库(如 pdfjs-dist 的 pdf.worker.mjs)通常会产出扩展名为 .mjs 的静态资源。

触发条件

  1. 前端应用通过动态 import() 或 <script type="module"> 引入 .mjs 静态资源。
  2. 静态资源托管在基于 Debian / Ubuntu 的 Nginx 服务器上,且未显式指定 .mjs 的 MIME 映射。
  3. 线上经由 CDN(如腾讯云 EdgeOne、Cloudflare 等)缓存加速。

异常症状

本地开发环境(如 Vite 开发服务器)正常工作,但部署上线后浏览器报错:

Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.
Error: Setting up fake worker failed: "Failed to fetch dynamically imported module: https://.../assets/pdf.worker-xxxx.mjs".

功能(如 PDF 预览器)无法初始化,永远挂起在加载状态。


2. 根本原因剖析 (Root Cause)

2.1 HTML 规范对 ESM 模块的严格 MIME 校验

根据 HTML 与 Fetch 标准,浏览器在加载 ES Module(type="module"、import()、Worker ESM)时,强制要求服务端的 Content-Type 响应头必须为合法的 JavaScript MIME 类型(如 text/javascript 或 application/javascript)。如果服务器返回兜底类型 application/octet-stream,浏览器将立即拒绝执行该模块。

2.2 Ubuntu 官方 Nginx 基础配置缺陷

Ubuntu / Debian 发行版的标准 /etc/nginx/mime.types 中,JavaScript 仅绑定了 js 扩展名:

application/javascript                           js;

由于没有包含 mjs,Nginx 会自动回退到 default_type application/octet-stream;。

2.3 CDN 强缓存的陷阱 (CDN Cache Trap)

如果静态资源配置了强缓存策略(如 Cache-Control: public, immutable, max-age=2592000),CDN 边缘节点会把错误的 Content-Type: application/octet-stream 缓存长达 30 天。即使随后修复了源站 Nginx,客户端请求如果命中 CDN 缓存,仍然会因为错误的历史响应头报错。


3. 彻底解决方案 (Resolution Steps)

步骤 1:修复源站 Nginx 全局 MIME 映射

修改 /etc/nginx/mime.types,将 mjs 加入到 application/javascript 中:

sudo sed -i 's/application\/javascript                           js;/application\/javascript                           js mjs;/' /etc/nginx/mime.types
sudo nginx -t
sudo systemctl reload nginx

验证源站响应头:

curl -k -I https://127.0.0.1/assets/xxx.mjs -H "Host: your-domain.com"
# 确保输出:Content-Type: application/javascript

步骤 2:代码层追加版本标识击穿 CDN 历史缓存

由于 CDN 对带参数的 URL 默认视为独立缓存,为了立竿见影且无缝让全球所有用户即刻生效,在引用 worker 或动态 module 时追加构建版本查询参数:

// 针对 Vite / Webpack 生成的 worker url 追加版本参数
const workerUrl = new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url).href + '?v=2';
pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;

这样重新编译打包后,动态导入的 URL 为 /assets/pdf.worker-xxxx.mjs?v=2,CDN 收到新请求时会穿透回源站获取正确的 application/javascript 响应头并缓存。


4. 下次如何快速识别 (Diagnostic Heuristics)

  • 特征 1:控制台红字明确提示 Strict MIME type checking is enforced for module scripts per HTML spec。
  • 特征 2:网络面板中该 .mjs 资源 HTTP 状态码为 200,但响应头中的 Content-Type 为 application/octet-stream。
  • 特征 3:本地运行正常,上线后报错,或者部分 CDN 节点正常部分节点报错。
  • 结论:100% 为 Nginx 缺失 .mjs MIME 声明,配合 CDN 历史强缓存未刷新。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web