在 Node.js 的生态系统中,存在两种主要的模块规范:CommonJS (CJS) 和 ES Modules (ESM)。
理解它们的区别对于现代前端和后端开发至关重要,因为 Node.js 正在从 CJS 全面转向 ESM。
1. CommonJS (CJS)
CommonJS 是 Node.js 诞生的默认模块系统,已经存在了十多年。
- 语法 :使用
require()导入模块,使用module.exports导出模块。 - 加载方式 :同步加载 。这意味着当你调用
require()时,Node.js 会阻塞后续代码的执行,直到模块加载完成。 - 适用场景:传统 Node.js 项目、服务器端脚本。
示例代码:
javascript
// 导出 (math.js)
const add = (a, b) => a + b;
module.exports = { add };
// 导入 (main.js)
const { add } = require('./math.js');
console.log(add(2, 3));
2. ES Modules (ESM)
ESM 是 JavaScript 在 2015 年(ES6)确立的标准官方模块系统,旨在让浏览器和服务器通用一套标准。
- 语法 :使用
import导入,使用export导出。 - 加载方式 :异步加载。它分为三个阶段:构建(解析)、链接、求值。这种机制支持"静态分析"。
- 适用场景:现代前端框架(Vue, React)、浏览器环境、现代 Node.js 项目。
示例代码:
javascript
// 导出 (math.js)
export const add = (a, b) => a + b;
// 导入 (main.js)
import { add } from './math.js';
console.log(add(2, 3));
3. 核心区别对比
| 特性 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 关键字 | require / module.exports |
import / export |
| 加载时机 | 运行时加载。 | 编译时加载(静态分析)。 |
| 加载方式 | 同步。 | 异步。 |
| 顶层变量 | 有 __dirname, __filename。 |
没有这些变量(需用 import.meta.url 模拟)。 |
| this 指向 | 指向当前模块。 | undefined。 |
| Tree Shaking | 不支持(因为是动态加载)。 | 支持(可以删除未使用的代码)。 |
| 严格模式 | 默认不开启。 | 强制开启 ("use strict")。 |
4. 关键差异深度解析
(1) 静态分析 vs 动态加载
- ESM 是静态的 :
import语句必须放在文件顶部。因为它是静态的,打包工具(如 Webpack, Rollup, Vite)可以在代码运行前知道哪些代码被使用了,从而进行 Tree Shaking(剔除无效代码),缩小包体积。 - CJS 是动态的 :你可以在
if语句或函数内部写require()。这使得它很灵活,但也无法在编译阶段进行优化。
(2) 值的拷贝 vs 值的引用
- CJS :导出的是值的拷贝。一旦输出一个值,模块内部的变化不会影响到已加载的值。
- ESM :导出的是值的动态只读引用(Live Bindings)。如果模块内部修改了变量,外部引用的地方也会同步更新(但外部不能修改它)。
(3) 环境变量(__dirname)
在 CJS 中,你可以直接使用 __dirname 获取当前目录。在 ESM 中,这些全局变量不存在,你需要这样操作:
javascript
// ESM 获取 __dirname 的方法
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
5. 如何在 Node.js 中选择?
现在 Node.js 同时支持两者,判断规则如下:
- 文件名后缀 :
.cjs总是被视为 CommonJS。.mjs总是被视为 ES Modules。.js取决于最近的package.json。
- package.json :
- 设置
"type": "module",则.js文件被视为 ESM。 - 设置
"type": "commonjs"或不设置,则.js文件被视为 CJS。
- 设置
6. 互操作性(能不能混用?)
-
ESM 导入 CJS :可以。
import cjs from './file.cjs'通常能工作,但只能默认导入,不能通过解构导入(因为 CJS 是运行时生成的)。 -
CJS 导入 ESM :不可以 使用
require()。因为 ESM 是异步的,而require是同步的。如果必须在 CJS 中用 ESM,只能使用异步的import()函数:javascript// 在 CJS 文件中 import('./esm-file.mjs').then(module => { // 使用模块 });
总结
- 如果你正在开发新项目 ,建议优先使用 ESM,因为它是未来的标准,且支持更好的性能优化。
- 如果你在维护老旧项目 或使用大量仅支持 CJS 的老旧库,则继续使用 CommonJS。