前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库浏览器 ESM 为什么要求 CORS
JavaScript模块与语言机制

为什么 script type=module 加载跨域脚本必须过 CORS,普通 script 却不用?

浏览器请求模块脚本时用的是 cors 模式,跨域的模块文件必须带 Access-Control-Allow-Origin 响应头,否则直接被拦。普通 <script> 是历史遗留的 no-cors 请求,可以随便跨域引用。模块要走 CORS,是因为导入方能直接读到模块导出的内容,等于读取了跨域资源,必须由对方明确授权。同源模块不受影响。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#模块与语言机制
先看核心答案读代码示例
理解线索

两种脚本请求的差别

  1. no-cors 普通脚本跨域能执行,但页面拿不到脚本的错误详情
  2. cors 模块脚本跨域必须由服务端响应头授权,否则加载失败
  3. crossorigin 属性模块默认按 anonymous 处理,要带 Cookie 就设成 use-credentials

双击打开本地 HTML 时页面源是 null,模块请求全部算跨域,而 file:// 没法返回 CORS 头,所以模块一个都加载不了。开发时要起一个本地服务器。

核心回答

先记住这个答案

浏览器用 <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 响应头。

普通脚本能用,模块被拦HTML
<!-- 普通脚本: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 一起来的几个要求

MIME 类型也更严格了。服务端返回模块文件时,Content-Type 必须是 JavaScript 类型,比如 text/javascript。返回 text/plain 或 application/octet-stream,浏览器会拒绝执行。这在一些把 .mjs 后缀识别成未知类型的静态服务器上很常见。

Cookie 默认不带跨域。模块请求相当于默认设了 crossorigin="anonymous",同源请求会带 Cookie,跨域不带。如果跨域模块需要登录态,要在标签上写 crossorigin="use-credentials",服务端同时返回 Access-Control-Allow-Credentials: true,并且 Access-Control-Allow-Origin 不能是 *。

回答前,多想一步

容易答错的地方

以为同源模块也需要配 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 属性要和真正加载时一致,否则预加载的结果匹配不上,浏览器会重新请求一次,白白多一次下载。

从一道题,走向一组知识

把知识连起来

模块与语言机制

一个模块里导出两个同名的变量会怎样?和默认导出能重名吗?

和本题关系不大,讲的是模块里重复导出同名变量的语法错误。

模块与语言机制

动态 import() 返回的是什么?为什么它是异步的?

动态 import() 加载跨域模块时同样走 CORS,被拦下时返回的 Promise 会拒绝。

参考资料

  • JavaScript:模块与语言机制参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 同一个 CDN 文件,两种引法
  3. 为什么模块必须走 CORS
  4. 跟着 CORS 一起来的几个要求
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑