Skip to content

ES Module 和 CommonJS 怎么区分? ​

ESM 的依赖是静态的:import / export 在代码运行前就能确定,因此可以做 tree shaking,并且浏览器能按模块地址去加载。CommonJS 的 require 是函数调用,发生在运行时,Node 传统包大多是这种格式。

原理 ​

ESM 文件默认严格模式。导入的绑定是活的:导出方后面改了这个绑定,导入方读到的是新值。循环依赖时,如果在导出初始化完成前就使用,会碰到暂时性死区。

js
// a.mjs
export const name = "a"
export function read() {
  return name
}

CommonJS 导出的是 module.exports 上的值,require 拿到的是当时的对象。循环依赖时可能拿到一个还没赋完值的 exports。它可以用条件判断决定要不要 require,这是运行时加载的灵活性,也是静态分析的障碍。

打包器的工作是把许多 ESM(以及转换后的 CJS)合成更少的文件,减少请求,并按入口拆包。开发时 Vite 尽量让浏览器直接使用 ESM;生产时再打包。见 Vite 与 Webpack。

包的入口要看 package.json 的 exports。只写 main 往往指向 CJS;module 或 exports.import 才是 ESM。一个包同时提供两种格式时,不要让两份实现各自持有状态,否则会打出两份单例。

追问 ​

为什么 tree shaking 要求 ESM?

摇树要在不运行代码的情况下知道哪个导出没人用。import { a } 是声明。require 的参数如果是变量,运行前无法确定会加载什么,打包器只能保守地留下。见 Tree Shaking。

浏览器里能直接用 ESM 吗?

可以,<script type="module"> 默认延迟执行,并且按图加载。裸导入说明符(import 'vue')浏览器不认识,需要导入映射或开发服务器重写。这就是 Vite 开发服务器在做的事。

易错点 ​

  • 在 ESM 里使用 __dirname 会直接报错。文件路径用 import.meta.url。
  • CJS 模块被默认导入时,打包器可能多包一层 .default。这是互操作,不是 ESM 又套了一层业务对象。
  • 循环依赖不是「ESM 才有」。两种格式都有,表现不同。

相关 ​