ESM(ECMAScript Modules)和 CJS(CommonJS)是 JavaScript 世界两套并存的模块系统。本页梳理它们的演进历史、五点核心差异、Node 与浏览器的立场差异、Vite/Metro 等打包工具的处理方式,以及 RN 里 require('./image.png') 的真实语义——它语法上是 CJS,但实际是 Metro 的 asset pipeline 借用了 CJS 语法。
概述:为什么需要模块系统
早期 JavaScript 没有模块概念。浏览器里用 <script> 标签按顺序加载文件,所有代码共享全局作用域,带来三个问题:
- 命名冲突:多个库定义同名全局变量就炸
- 依赖隐式:代码里看不出谁依赖谁
- 加载顺序靠人维护
模块系统要解决这三个问题——提供独立作用域、显式声明依赖、自动处理加载顺序。
ESM 是 ECMAScript 2015(ES6)标准化的官方模块语法;CJS 是 Node.js 在 2009 年自造的约定,早于 ECMAScript 规范。两者并存是历史偶然,但短期内无法统一。
工作方式:五个本质差异
1. 静态 vs 动态
ESM import 是静态语法——必须写在文件顶部,路径必须是字面量,不能放在 if 里。CJS require() 是普通函数,可以在任何位置、接受任意表达式参数。
这个限制是 ESM 最大的优势来源:静态可分析 = 打包工具能在编译期建立完整依赖图,做 tree-shaking、循环依赖检测、代码分割等优化。CJS 因为动态,打包工具不敢猜你用了什么,只能全量打包。
2. 同步 vs 异步
CJS 的 require() 是同步函数,立即读文件→编译→执行→返回值。适合服务器场景(文件在本地磁盘)。
ESM 的 import 是声明,实际加载由运行时调度,可以异步并行。浏览器友好,不阻塞 UI。
3. 值拷贝 vs 活引用(live binding)
// CJS: 导入是值快照
let count = 0
module.exports = { count }
// 另一文件 import 后 count 始终是 0,即使原模块修改了它
// ESM: 导入是活引用
export let count = 0
// 另一文件 import count 后,读取总是拿到最新值
ESM 的 export 是绑定(binding),这个设计让循环依赖能正确工作。
4. 文件类型判断
Node.js 根据以下规则区分:
| 规则 | 结果 |
|---|---|
.mjs | ESM |
.cjs | CJS |
.js + package.json 有 "type": "module" | ESM |
.js + 无此字段或 "type": "commonjs" | CJS |
大多数老项目没设 "type": "module",所以 .js 默认是 CJS。
5. 顶层 this
CJS 里顶层 this === module.exports,ESM 里顶层 this === undefined。
语法速查
| 场景 | CJS | ESM |
|---|---|---|
| 命名导出 | module.exports.foo = ... | export const foo = ... |
| 默认导出 | module.exports = X | export default X |
| 命名导入 | const { foo } = require('./m') | import { foo } from './m' |
| 默认导入 | const M = require('./m') | import M from './m' |
| 重命名 | const { foo: bar } = require(...) | import { foo as bar } from '...' |
| 导入所有 | 无直接对应 | import * as M from '...' |
| 动态加载 | require(expr) | const m = await import(expr) |
互操作的痛点
- ESM 可以 import CJS 包:Node 把 CJS 的
module.exports包装成 default export - CJS 不能 require ESM 包:
require()同步返回,ESM 异步加载,语言设计层面的硬限制 - 解决办法:CJS 里用
await import()动态导入 ESM
报错 ERR_REQUIRE_ESM 就是这个互操作问题的典型表现。
RN 里的 require:一个 Metro 特例
RN 代码里常见这种写法:
<Image source={require('./logo.png')} />
语法上是 CJS 的 require()。语义上不是 CJS 模块加载——它是 Metro bundler 在构建时做的特殊转换:
- 构建期:把图片文件注册到 asset registry,分配数字 ID
- 代码转换:把
require('./logo.png')替换成对注册表的调用 - 打包期:复制图片到 iOS
.xcassets或 Androidres/drawable-*dpi/ - 运行期:
<Image>拿 ID,让 native 层读取对应图片
为什么不用 import
Metro 其实也支持 import logo from './logo.png',但 RN 惯例一直用 require(),有两个原因:
- 历史惯性:官方文档从早期就用 require
- 动态灵活:require 可以写在表达式里
// require 能写在条件分支
const logo = isDark
? require('./logo-dark.png')
: require('./logo-light.png')
// import 必须写在顶部,等价写法要先 import 两个再三元选一个
不支持动态路径
// ❌ 报错
const name = isDark ? 'logo-dark' : 'logo-light'
<Image source={require(`./${name}.png`)} />
Metro 的 asset transformation 是编译期做的,必须静态看出要 require 哪个文件。动态路径意味着运行时才知道路径,Metro 没法提前把图片打进 bundle。
打包工具的立场
| 工具 | 输入 | 开发期策略 |
|---|---|---|
| Webpack | 都支持 | 全量打包再增量更新 |
| Rollup | ESM 优先 | 输出 ESM / CJS / UMD |
| esbuild | 都支持 | 速度优先 |
| Vite | ESM 优先 | 开发期不打包,利用浏览器原生 ESM 按需请求 |
| Metro | 都支持 | RN 专用,深度集成 native asset |
Vite 速度的秘密:开发期不打包。浏览器支持 ESM 后,Vite 让浏览器按 import 关系按需请求每个文件,单文件独立缓存。修改一个文件只重编一个。Webpack 早期心智是”全量打包”,模块多了启动就慢。
Metro 的独特点:它是 RN 专用的 bundler,深度集成 native asset(iOS xcassets、Android res)、platform 分发(.ios.js / .android.js)、Hermes 字节码预编译。这些是 Webpack/Vite 做不到的。
生态现状(2026)
- Node.js 12(2019)实验支持 ESM,Node 16(2021)稳定
- npm 上仍约 70% 的包以 CJS 为主入口
- 一批新包改用 ESM-only(
node-fetch@3、chalk@5、got@12) - Node 20+ 支持实验性
require(esm) - 大趋势是 ESM 取代 CJS,过程可能还要 3-5 年
实践建议
- 新项目:TS/业务代码用 ESM;Node 生态的配置文件不得不用 CJS(Jest、Tailwind、Babel、Metro 的
.config.js) - 写库:考虑输出双格式(
main+module+exports条件导出) - 遇到
ERR_REQUIRE_ESM:用await import()代替require() - RN 资源:用
require(),享受 Metro asset pipeline,记住不支持动态路径
参见
- 2026-04-13-Metro Hermes 与 JSI — Metro bundler 的三阶段流水线
- 2026-04-13-Expo vs 裸 RN — Expo 工作流和打包体系
- 2026-04-13-单元测试基础与 Jest 配置 — Jest 在 ESM 下的特殊处理
参考
- ECMAScript Modules Specification — ESM 官方规范
- Node.js Modules: ECMAScript modules — Node 的 ESM 文档
- Node.js Modules: CommonJS modules — Node 的 CJS 文档
- Sindre Sorhus: Pure ESM package — 为什么大量包在迁移 ESM-only
- Metro: Asset References — RN 资源加载文档
- Axel Rauschmayer: ECMAScript modules — 深度但易读的 ESM 教程