模块加载器处理混合依赖的核心是运行时桥接层,而非自动转换;它通过文件后缀与package.json type字段共同决策模块类型,在ESM与CJS间建立可预测映射:ESM导入CJS默认暴露module.exports为default,CJS require ESM则必须用动态import()或构建时包装。

模块加载器处理混合依赖,核心不是“自动转换”,而是通过运行时桥接层在两种规范语义之间建立可预测的映射关系。它不改变原始模块行为,而是让导入方能以一致方式访问导出内容。
ESM 导入 CJS:default 互操作是默认桥梁
Node.js 和现代构建工具(如 Vite、Webpack)对 import cjs from 'cjs-pkg' 这类语法有统一约定:将 CJS 的 module.exports 对象整体作为 ESM 的 default 导出暴露。
- CJS 模块导出
module.exports = { a: 1, b: 2 }→ ESM 中import cjs from 'x'得到的就是这个对象,cjs.a可直接用 - 若 CJS 写成
module.exports = { default: 'value' },则import cjs from 'x'得到的是{ default: 'value' },需写cjs.default - 这种桥接由加载器在 Link 阶段注入,不修改源码,也不执行 CJS 模块顶层代码(直到 Evaluate 阶段)
CJS require ESM:必须走动态 import() 或包装器
CommonJS 的 require() 是同步阻塞调用,而 ESM 加载本质异步(Load → Link → Evaluate 全链 Promise 化),因此 Node.js 不允许 require('./mod.mjs') 直接调用。
- Node.js 会抛错:
ERR_REQUIRE_ESM,明确禁止同步加载 ESM - 正确方式是用
await import('./mod.mjs'),交由 ESM 加载器异步完成三阶段流程 - Vite 等工具在预打包时,会把 ESM 依赖转为 CJS 兼容格式(例如包裹进
__commonJS函数),再供 CJS 模块require—— 这属于构建时桥接,非运行时原生能力
双模共存的关键:文件后缀与 package.json type 共同决策
Node.js 判断模块类型,优先级是:文件后缀 > 同级 package.json 的 "type" 字段 > 父级 package.json。
-
.mjs文件永远按 ESM 解析,.cjs永远按 CJS 解析,不受type影响 -
.js文件则严格看最近的package.json中"type": "module"或"type": "commonjs" - 混合项目中常见做法:根目录设
"type": "module",CJS 第三方库所在子目录放一个{"type": "commonjs"}的package.json,实现局部降级
循环引用场景下,桥接机制暴露语义差异
CJS 和 ESM 处理循环依赖的方式根本不同,桥接层无法抹平这一差异,只能如实传递。
- CJS 中,
require('a')在a.js执行中途返回未完成的module.exports对象 → 可能读到部分初始化值 - ESM 中,
import触发的是完整模块图拓扑排序,循环依赖模块的顶层声明(export let x)会先分配内存槽位,但求值延迟 → 导入方可能拿到undefined - 当 ESM
import一个 CJS 模块,而该 CJS 又require回一个 ESM 模块时,Node.js 会拒绝并报错,因为无法协调两种生命周期模型


















