type="module" 切换脚本执行环境为ES模块规则,要求显式路径、禁止裸导入、默认defer且作用域隔离,modulepreload仅预下载不执行,模块加载含解析/实例化/执行三阶段。

type="module" 是告诉浏览器:“这段脚本要用 ES 模块规则来解析”,不是加个属性就完事——它直接切换了整个执行环境的底层行为。
为什么加了 type="module" 还报 SyntaxError: Cannot use import statement outside a module
这通常不是配置问题,而是加载上下文错了:
- 写了
import却放在<script></script>(没写type="module")里,浏览器按传统脚本执行,自然不认import - 写了
<script type="module">import('./a.js')</script>,但里面用了动态导入import(),而该脚本本身没被标记为模块——import()只能在模块上下文中调用 - 本地双击打开 HTML(
file://协议),Chrome/Firefox 会直接拒绝加载模块脚本,连请求都不发,控制台只显示 CORS 错误或空错误
路径必须带扩展名、不能省略 ./ 或 ../,否则 import 失败
ES 模块规范强制路径静态化,浏览器不会自动补 .js,也不会把裸名当相对路径处理:
-
import { foo } from './utils.js'✅ 正确:显式扩展名 + 相对路径前缀 -
import { foo } from './utils'❌ 报错:Failed to load module script -
import { foo } from 'lodash'❌ 静态解析失败,除非配了<script type="importmap"></script> -
import { foo } from 'utils.js'❌ 裸路径,浏览器无法判断是相对还是绝对,直接 SyntaxError
模块脚本默认 defer,但和传统 defer 有本质区别
type="module" 脚本天生具备 defer 行为,但它的“延迟”逻辑更严格:
- 下载与 HTML 解析并行,但执行一定在 DOM 构建完成之后(等价于
DOMContentLoaded) - 多个
type="module"脚本按出现顺序执行,哪怕它们是异步加载的——这是模块图决定的,不是靠 DOM 顺序模拟 - 它不会像传统
defer那样共享全局作用域:每个模块顶层声明的const、function都不会挂到window上 - 如果你依赖
document.currentScript获取当前脚本标签,模块里拿不到,它始终是null
modulepreload 不是“让模块更快”,而是“让模块提前进缓存”
和 type="module" 是配合使用的两个独立机制:
-
modulepreload只发起网络请求,不执行、不解析、不建立模块图——纯下载调度 - 它要求
href必须和后续<script type="module" src="..."></script>的src完全一致(含查询参数) - 跨域模块(比如 CDN 上的
lodash-es)必须加crossorigin属性,否则预加载静默失败 - 它不递归预加载依赖项:如果
main.mjs里import './utils.mjs',你得手动再写一行
真正容易被忽略的点是:模块加载不是“下载完就执行”,而是三阶段(解析 → 实例化 → 执行),其中实例化阶段就完成了导出绑定的“活引用”——改一个模块里的 export let count = 0,另一个模块里 import { count } 看到的就是实时值。这个机制在调试循环依赖或状态同步时,常常成为隐性陷阱。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











