先记住这个答案
浏览器用 <script type="module"> 或 import 请求模块时,请求模式是 cors。跨域的模块文件,服务端必须返回 Access-Control-Allow-Origin 允许当前页面的源,否则浏览器直接拦下,模块不执行。普通 <script> 是 no-cors 请求,这是历史兼容留下的口子,跨域引 jQuery 从来不需要对方授权。模块之所以收紧,是因为导入方可以直接读到模块导出的值,这就是读取跨域资源的内容,按同源策略必须对方同意。
- 模块脚本用
cors模式请求,跨域必须有Access-Control-Allow-Origin - 普通
<script>是no-cors,跨域不需要授权 - 同源模块不受影响,本地
file://打开会失败 - 默认不带 Cookie,需要时加
crossorigin="use-credentials"
同一个 CDN 文件,两种引法
假设页面在 https://a.com,要引用 https://cdn.b.com 上的一个脚本,这个 CDN 没有配置 CORS 响应头。
<!-- 普通脚本:no-cors 请求,能正常执行 -->
<script src="https://cdn.b.com/lib.js"></script>
<!-- 模块脚本:cors 请求,响应没有 Access-Control-Allow-Origin,加载失败 -->
<script type="module" src="https://cdn.b.com/lib.mjs"></script>
<!-- 模块里 import 也一样 -->
<script type="module">
import { format } from 'https://cdn.b.com/utils.mjs' // 同样被 CORS 拦下
</script>控制台会出现类似「has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header」的报错。解决办法只能在服务端:给模块文件的响应加上 Access-Control-Allow-Origin: * 或者指定页面的源。unpkg、jsDelivr、esm.sh 这些公共 CDN 默认都带了这个头,所以直接 import 它们的地址没问题。
为什么模块必须走 CORS
同源策略的核心是:页面不能读取其他源的内容,除非对方授权。普通脚本其实是个漏洞,跨域脚本执行后定义的全局变量页面能读到,早年就有人利用 JSONP 风格的数据文件窃取信息。但这个行为太多网站依赖了,改不掉。
模块是新规范,没有历史包袱,于是一开始就按正确的方式设计。import 能拿到模块的每个导出,相当于读取了文件内容,所以跨域加载要对方用 CORS 响应头明确同意。
还有一个好处是错误信息。跨域的普通脚本出错,window.onerror 只能拿到一句 Script error.,看不到具体行号和堆栈。模块走了 CORS,报错信息是完整的,前端监控能拿到真实错误。
容易答错的地方
- 以为同源模块也需要配 CORS 头
- 同源请求不涉及跨域检查,自己站点下的模块不用任何配置。只有模块文件放在别的域名,比如独立的静态资源 CDN 域名,才需要配。
- 以为给
<script>加上crossorigin就能绕过 crossorigin只是控制请求带不带凭证,不能跳过 CORS 检查。服务端没返回允许的响应头,加什么属性都没用。- 以为本地打不开是代码写错了
- 双击 HTML 用
file://打开,页面的源是null,所有模块请求都算跨域,而文件协议没法返回 CORS 头,所以一定失败。用npx serve或 Vite 开发服务器起一个本地服务就好了。
面试官还会怎么问?
动态 import() 加载跨域模块也要 CORS 吗?
要。动态 import() 和静态 import 走的是同一套模块加载流程,请求模式同样是 cors。被拦下时,import() 返回的 Promise 会以 TypeError 拒绝,可以在 catch 里处理。
为什么打包后的项目很少碰到这个问题?
打包工具把模块合并成了普通脚本或者和页面同源的文件。只有代码分割出的 chunk 放到了别的 CDN 域名,而入口用的是 type="module" 时,才需要给 CDN 配 CORS。Vite 构建出的入口就是 type="module",把产物放到独立静态资源域名时要记得配。
<link rel="modulepreload"> 预加载模块要注意什么?
它的请求也是 cors 模式,crossorigin 属性要和真正加载时一致,否则预加载的结果匹配不上,浏览器会重新请求一次,白白多一次下载。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。