Q1: CommonJS 和 ES Module 有什么区别?为什么 Node.js 会转向 ESM?
约 2524 字大约 8 分钟
Node.jsCommonJSES ModulesTypeScript
2026-02-13
问题背景
作为 Node.js 开发者,我经常在项目中同时看到 require() 和 import 语句,对两者的区别和演进历史感到困惑。具体来说:
- CommonJS 和 ES Module 的核心区别是什么?
- 为什么 Node.js 要从 CommonJS 转向 ESM?
- TypeScript 的 import/export 是否也是 ESM?
权威资料搜集
1. Node.js 官方文档分析
关键区别总结:
2. Node.js 源码实现对比
通过分析 Node.js 源码(基于文档描述):
CommonJS 加载流程:
// 伪代码表示
function require(id) {
// 1. 解析路径 → 2. 检查缓存 → 3. 创建模块实例
// 4. 包裹函数编译 → 5. 执行代码 → 6. 返回 exports
}ESM 加载流程:
// 伪代码表示
async function loadESM(specifier, parentURL) {
// 1. 解析为 URL → 2. 获取源码 → 3. 解析依赖图
// 4. 递归加载依赖 → 5. 实例化 → 6. 评估执行
}3. 核心贡献者观点
Ryan Dahl (Node.js 创始人) - 通过 Deno 博客
在 Deno 博客文章中,Ryan 明确指出:
"CommonJS was exactly what JavaScript needed in 2009... But with ESM as the standard and the focus shifting towards cloud primitives — the edge, browsers, and serverless compute — CommonJS simply doesn't cut it."
他指出的 CommonJS 问题:
- 同步加载:阻塞主线程,不适合现代 Web
- 难以 tree-shake:导致打包体积过大
- 非浏览器原生:需要构建工具转换
- 与 Web 标准脱节:ESM 是 JavaScript 语言标准
Myles Borins (Node.js 模块团队)
在 2017 年的文章中提到:
"We envision a future where after installing a module, developers will be able to run the code in Node.js or the Browser without requiring a build step."
4. 历史演进分析
交叉验证
一致性检查
- 所有来源都确认:ESM 是 JavaScript 语言标准,CommonJS 是 Node.js 特定实现
- 所有来源都确认:ESM 支持静态分析,CommonJS 是动态加载
- 所有来源都确认:ESM 设计时考虑了浏览器兼容性
矛盾点分析
唯一的分歧:迁移速度
- 保守派:CommonJS 生态庞大,迁移需要时间
- 激进派:CommonJS 阻碍 JavaScript 生态发展
但共识是:ESM 是未来方向。
深度解答
1. CommonJS 和 ESM 的核心区别
用餐厅点餐来比喻:
CommonJS:像在餐厅现场看菜单点菜(运行时决定)
// 根据条件动态选择模块 let module; if (condition) { module = require('./module-a'); } else { module = require('./module-b'); }ESM:像提前预订套餐(编译时决定)
// 必须在顶层静态导入 import moduleA from './module-a.js'; import moduleB from './module-b.js'; // 使用哪个在运行时决定
技术层面的核心差异:
| 维度 | CommonJS | ESM | 影响 |
|---|---|---|---|
| 加载时机 | 运行时 | 编译时 | ESM 可优化,支持 tree-shaking |
| 依赖解析 | 执行到 require() 时解析 | 加载时解析所有依赖 | ESM 可并行加载 |
| 作用域 | 函数作用域(被包裹) | 模块作用域 | ESM 更符合 ES6 标准 |
| 循环依赖 | 支持但可能有问题 | 静态分析避免问题 | ESM 更安全 |
2. 为什么 Node.js 要从 CommonJS 转向 ESM?
四个核心原因:
原因一:标准化需求
- ESM 是 ECMAScript 标准,CommonJS 是社区规范
- 浏览器原生支持 ESM,实现前后端统一
- TypeScript、Deno、Bun 等现代运行时都优先支持 ESM
原因二:性能优势
// ESM 的静态分析支持优化
import { functionA } from './module.js';
// 打包工具可以只包含 functionA,移除未使用的代码
// CommonJS 难以优化
const utils = require('./utils');
// 打包工具必须包含整个 utils 模块原因三:现代开发体验
- Top-level await:ESM 支持,CommonJS 不支持
- 更好的工具链支持:Vite、esbuild 等现代工具为 ESM 优化
- 模块联邦:微前端等现代架构依赖 ESM
原因四:生态演进压力
Ryan Dahl 在 Deno 博客中指出:
"The JavaScript ecosystem is being held back by the need to support both module systems."
3. TypeScript 的 import/export 是否也是 ESM?
答案是:看 tsconfig.json 配置!
根据 TypeScript 官方文档:
情况一:输出为 ESM
// tsconfig.json
{
"compilerOptions": {
"module": "ESNext", // 或 "ES2020", "ES2022" 等
"target": "ES2020"
}
}此时:TypeScript 的 import/export 编译为 ESM 语法。
情况二:输出为 CommonJS
// tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2020"
}
}此时:TypeScript 的 import/export 编译为 CommonJS 的 require()/module.exports。
关键机制:esModuleInterop
{
"compilerOptions": {
"esModuleInterop": true
}
}这个标志让 TypeScript 更好地处理 CommonJS 和 ESM 的互操作。
实际编译示例:
// TypeScript 源码
import fs from 'fs';
import { readFile } from 'fs';
// 编译为 CommonJS (module: "CommonJS")
const fs = require('fs');
const { readFile } = require('fs');
// 编译为 ESM (module: "ESNext")
import fs from 'fs';
import { readFile } from 'fs';实践建议
1. 新项目选择
// package.json
{
"type": "module", // 使用 ESM
"engines": {
"node": ">=18.0.0" // 完整 ESM 支持
}
}2. 旧项目迁移策略
// 渐进式迁移
// 1. 先设置 "type": "module"
// 2. 将 .js 文件重命名为 .cjs
// 3. 逐步将文件转换为 .mjs 或使用 ESM 语法3. 库作者的最佳实践
// 支持双模式
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
}
}4. 常见问题解决
// 问题:ESM 中没有 __dirname
// 解决方案:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 问题:动态导入
// CommonJS: require(`./${name}.js`)
// ESM: import(`./${name}.js`) // 返回 Promise扩展思考
1. 为什么迁移这么慢?
- 生态惯性:npm 上有数百万个 CommonJS 包
- 工具链兼容性:Webpack、Jest 等工具需要时间适配
- 学习成本:开发者需要理解两种系统
2. Deno 和 Bun 的启示
- Deno:从一开始就只支持 ESM
- Bun:同时支持但推荐 ESM
- 趋势:新运行时都优先考虑 ESM
3. 未来的模块系统
可能会出现的特性:
- Import maps:更灵活的模块解析
- HTTP imports:直接导入远程模块
- 更好的 tree-shaking:基于 ESM 的静态分析
总结
- CommonJS 和 ESM 本质不同:一个是运行时动态加载,一个是编译时静态加载
- Node.js 转向 ESM 是必然:为了标准化、性能、现代开发体验
- TypeScript 是语法糖:编译目标决定最终模块系统
- 迁移需要策略:渐进式、双模式支持、工具链更新
最简记忆:
require()→ 同步,动态,Node.js 特有import→ 异步,静态,JavaScript 标准- TypeScript → 编译时决定最终形式
参考资料
官方文档
Node.js ES Modules Documentation - Node.js v25.6.1 官方文档
- 链接:https://nodejs.org/api/esm.html
- 时间:2026年2月13日查阅
- 内容:ESM 与 CommonJS 的详细对比、互操作性、加载算法
TypeScript Modules Handbook - TypeScript 官方手册
- 链接:https://www.typescriptlang.org/docs/handbook/2/modules.html
- 时间:2026年2月13日查阅
- 内容:TypeScript 模块语法、编译选项、与 CommonJS/ESM 的关系
核心贡献者观点
CommonJS is hurting JavaScript - Ryan Dahl (Deno 博客)
- 链接:https://deno.com/blog/commonjs-is-hurting-javascript
- 时间:2023年发布,2026年2月13日查阅
- 内容:CommonJS 历史、问题分析、ESM 优势
The Current State of Implementation and Planning for ESModules - Myles Borins
- 链接:https://medium.com/the-node-js-collection/the-current-state-of-implementation-and-planning-for-esmodules-a4ecb2aac07a
- 时间:2017年发布
- 内容:Node.js ESM 实现规划与愿景
技术深度分析
A Deep Dive Into CommonJS and ES Modules in Node.js - AppSignal Blog
- 链接:https://blog.appsignal.com/2024/12/11/a-deep-dive-into-commonjs-and-es-modules-in-nodejs.html
- 时间:2024年12月11日发布
- 内容:两种模块系统的技术对比、性能分析
CommonJS vs. ES modules in Node.js - LogRocket Blog
- 链接:https://blog.logrocket.com/commonjs-vs-es-modules-node-js/
- 时间:2024年6月20日发布
- 内容:实际应用场景、迁移策略
社区讨论
CommonJS vs ES Modules and why? - Reddit r/node 讨论
- 链接:https://www.reddit.com/r/node/comments/zgred2/common_js_vs_es_modules_and_why/
- 时间:2022年12月讨论
- 内容:开发者实际经验、迁移痛点
Supporting CommonJS and ESM with Typescript and Node - 技术博客
- 链接:https://evertpot.com/universal-commonjs-esm-typescript-packages/
- 时间:2026年2月13日查阅
- 内容:双模式库开发实践
历史背景
What Server Side JavaScript needs - Kevin Dangoor (2009)
- 链接:https://www.blueskyonmars.com/2009/01/29/what-server-side-javascript-needs/
- 时间:2009年1月29日
- 内容:CommonJS 诞生的背景和需求
A brief history of ES modules - DEV Community
- 链接:https://dev.to/dodson/a-brief-history-of-es-modules-2fld
- 时间:2024年11月16日
- 内容:ESM 标准发展历史
下一篇问题征集:你有什么 Node.js 相关的深度问题想要探索?欢迎在评论区留言!
