type="module" 本身不触发降级,因旧浏览器直接忽略该标签而不执行或报错;必须配合 nomodule 属性实现兼容:现代浏览器执行 type="module" 并跳过 nomodule 脚本,旧浏览器则忽略 type="module" 而执行 nomodule 脚本。

旧浏览器不支持 type="module",直接忽略带该属性的 <script></script> 标签——所以不能只靠它来降级,必须配合 nomodule 显式声明“非模块脚本”。
为什么 type="module" 本身不触发降级
Chrome 61+、Firefox 60+ 等现代浏览器会执行 <script type="module"></script>,但 IE、Edge 12–18、Safari 10.1 之前等老版本压根不认识这个 type 值,直接跳过该标签,什么也不做。它不会报错,也不会 fallback 到 text/javascript,更不会自动加载另一个脚本。
常见错误现象:
– 页面在 IE11 中完全没 JS 执行(开发者工具里看不到脚本加载)
– 以为加了 type="module" 就“自动兼容”,结果老用户白屏或功能缺失
- 模块脚本默认是
defer行为,且无法通过document.write注入 - 老浏览器看到
type="module"就静默丢弃,不解析、不下载、不报错 - 仅靠
type="module"无法实现任何降级逻辑,它只是“开关”,不是“桥接器”
nomodule 是怎么配合工作的
nomodule 是一个布尔属性,**仅被支持 type="module" 的浏览器识别并遵守**:当浏览器决定执行某个 type="module" 脚本时,它会跳过所有带 nomodule 的 <script></script> 标签;反之,不支持模块的浏览器会忽略 type="module",但会正常加载并执行带 nomodule 的脚本。
典型写法:
<script type="module" src="app.mjs"></script><script nomodule src="app.js"></script>
说明:
– app.mjs 是 ES 模块格式(含 import/export),供现代浏览器用
– app.js 是编译后的 IIFE 或 UMD 格式,无 import,可直接执行
– Safari 10.1 会执行 app.js,Safari 11+ 会执行 app.mjs 并跳过 app.js
一款AI工具,主要用于生成可直接复制粘贴的 Bash 脚本,用于 Ralph Wiggum/AI 代理循环(Codex、Claude Code、OpenCode、Goose)。适用于“拉尔夫循环”“Ralph Wiggum 循环”或 AI 循环请求,依据 PROMPT.md、AGENTS.md、SPECS、IMPLEMENTATION_PLAN.md 进行计划/构建,包含计划与构建模式、背压、沙箱及完成条件,适合需要提升相关任务效率的用户。
-
nomodule必须和type="module"成对出现才有意义,单独写没效果 - 两个
<script></script>标签需放在同一文档位置(顺序无关,但建议模块在前) - Webpack/Vite 默认输出的
index.html模板通常已内置该双标签结构
实际部署时容易踩的坑
你以为写了 nomodule 就万事大吉?这些细节常导致老浏览器仍加载失败:
- 打包产物中
app.js仍含import语法(比如忘了配 Babel + @babel/preset-env 的targets.ie: "11") -
app.js依赖了未 polyfill 的 API(如Promise、fetch、Array.from),IE11 直接报undefined is not a function - 用了动态
import()却没配置dynamic-import-node插件,导致app.js里残留import()调用 -
nomodule脚本用了type="text/javascript"—— 不必要,HTML5 规范中无type或type="text/javascript"效果相同,但多写反而易出错
验证方法:用 IE11 或 Edge 18 的 DevTools → Network 面板,确认加载的是 app.js 而非 app.mjs,再断点检查是否真能执行到首行代码。
如果连 nomodule 都不支持怎么办
极老浏览器(如 IE9/10)连 nomodule 属性都不识别,会把它当普通属性忽略,从而同时加载两个脚本——这会导致重复执行、变量冲突甚至报错。
此时必须用脚本检测兜底,例如:
<script>
if (!('noModule' in HTMLScriptElement.prototype)) {
var s = document.createElement('script');
s.src = 'app-legacy.js';
document.head.appendChild(s);
}
</script>
注意:
– 这段内联脚本必须放在所有模块/非模块 <script></script> 标签之前
– app-legacy.js 应彻底移除 ES6+ 语法,并注入 core-js 或 promise-polyfill
– 现代构建工具(如 Vite 的 build.target 设为 es2015)一般已覆盖此场景,但定制化强的项目仍需手动校验
真正麻烦的不是写法,而是你得确认那个“老用户”到底卡在哪个环节:是没加载?加载了但语法错?还是加载了、语法对、但 API 缺失?每一步都得对应验证,不能只看标签有没有写对。










