以下是针对初学者的 Node.js 模块系统详解,用通俗语言和代码示例解释 CommonJS 与 ES Module(ESM)的核心概念、区别及使用场景,帮你快速理清思路:
一、什么是模块系统?
模块系统是代码组织的规则,用于拆分代码为独立文件 (模块),并通过特定语法导入/导出功能。Node.js 早期用 CommonJS,现代前端和 Node.js 推荐用 ES Module(ESM)。
二、CommonJS:Node.js 的"老伙计"
核心特点
- 同步加载:模块加载时阻塞代码执行(Node.js 服务端场景友好,因为文件多在本地)。
- 运行时动态:模块的导入/导出在代码运行时确定(动态性强,但无法静态分析)。
- 语法简单 :用
require()
导入,module.exports
或exports
导出。
基本用法
1. 导出模块(math.js
)
ini
// math.js
const add = (a, b) => a + b;
const PI = 3.14;
// 导出单个值(覆盖默认导出)
exports.add = add;
// 导出多个值(对象形式)
module.exports = { PI };
2. 导入模块(app.js
)
javascript
// app.js
const math = require('./math.js'); // 导入整个模块
console.log(math.add(2, 3)); // 输出 5
console.log(math.PI); // 输出 3.14(注意:这里会报错!因为上面 exports.add 覆盖了 module.exports)
注意 :exports
是 module.exports
的引用,若直接赋值 exports = { ... }
会断开引用,导致导出失败。正确做法是统一用 module.exports
:
javascript
// 正确导出(math.js)
module.exports = {
add: (a, b) => a + b,
PI: 3.14
};
适用场景
- 旧项目迁移:早期 Node.js 项目(如 Express 4.x 及之前)普遍使用 CommonJS。
- 依赖 CommonJS 的库 :部分第三方库(如
lodash
早期版本)仅支持 CommonJS。 - 简单脚本:无需复杂静态分析的小工具(如本地脚本)。
三、ES Module(ESM):现代 JavaScript 的"新标准"
核心特点
- 异步加载:模块加载不阻塞代码执行(浏览器端原生支持,Node.js 中需配置)。
- 静态分析:导入/导出在编译时确定(支持 Tree Shaking,减少打包体积)。
- 语法更严谨 :用
import
/export
声明式导入导出,支持命名导出、默认导出。
基本用法
1. 导出模块(math.mjs
或 math.js
+ type: "module"
)
javascript
// math.js(需在 package.json 中配置 "type": "module")
// 命名导出(多个功能)
export const add = (a, b) => a + b;
export const PI = 3.14;
// 默认导出(一个主要功能)
export default function multiply(a, b) {
return a * b;
}
2. 导入模块(app.js
)
javascript
// app.js(同样需配置 "type": "module")
// 导入命名导出(需用大括号)
import { add, PI } from './math.js';
console.log(add(2, 3)); // 输出 5
console.log(PI); // 输出 3.14
// 导入默认导出(无需大括号)
import multiply from './math.js';
console.log(multiply(2, 3)); // 输出 6
// 导入所有导出(用 * 接收)
import * as MathUtils from './math.js';
console.log(MathUtils.PI); // 输出 3.14
Node.js 中使用 ESM 的条件
- 文件扩展名 :使用
.mjs
(明确声明为 ESM)。 - package.json 配置 :在项目根目录的
package.json
中添加"type": "module"
(所有.js
文件视为 ESM)。type默认是commonjs,即允许 require
。
四、CommonJS 与 ESM 的核心区别
特性 | CommonJS | ES Module(ESM) |
---|---|---|
加载方式 | 同步(运行时阻塞) | 异步(编译时加载,不阻塞) |
语法 | require() / module.exports |
import / export |
动态性 | 动态(运行时可修改导出) | 静态(编译时确定,无法动态修改) |
作用域 | 每个文件独立模块作用域 | 每个文件独立模块作用域 |
Tree Shaking | 不支持(无法静态分析依赖) | 支持(仅打包实际使用的代码) |
浏览器支持 | 不支持(仅 Node.js) | 原生支持(现代浏览器) |
Node.js 支持 | 原生支持(无需配置) | 需配置 .mjs 或 package.json |
五、推荐使用场景
1. 优先选 ESM 的场景
- 新项目:现代 JavaScript 项目(如 React、Vue、Vite 构建的项目)默认用 ESM。
- 前端开发:浏览器原生支持 ESM,与打包工具(Webpack、Vite)兼容更好。
- 需要 Tree Shaking:减少打包体积(如生产环境优化)。
- 静态分析工具:如 TypeScript、ESLint 能更精准分析 ESM 依赖。
2. 可选 CommonJS 的场景
- 旧项目迁移:已有大量 CommonJS 代码的项目(逐步迁移更稳妥)。
- 依赖 CommonJS 的库 :部分第三方库(如
express
早期版本)仅支持 CommonJS。 - 简单脚本:无需复杂打包的小工具(如本地自动化脚本)。
3. 混合使用(Node.js 中)
Node.js 支持在 ESM 中导入 CommonJS 模块(反之不行):
javascript
// ESM 文件(app.js)
import cjsModule from './commonjs-module.cjs'; // 导入 CommonJS 模块(需 .cjs 扩展名)
console.log(cjsModule.add(2, 3));
六、常见问题与避坑指南
1. 报错:"Cannot use import statement outside a module"
-
原因 :在 ESM 中使用了
require()
,或在普通.js
文件中用了import
(未配置type: "module"
)。 -
解决:
- 改用
import
语法,或在.js
文件所在目录的package.json
中添加"type": "module"
。 - 若需混合使用,CommonJS 模块用
.cjs
扩展名(Node.js 识别为 CommonJS)。
- 改用
2. 报错:"module.exports is not defined"
- 原因 :在 ESM 文件中用了
module.exports
(ESM 不支持此语法)。 - 解决 :改用
export
语法(如export default
或export const
)。
3. 动态导入(import()
)
-
场景:需要根据条件动态加载模块(如按需加载)。
-
用法:
javascript// 动态导入(返回 Promise) const loadModule = async () => { const module = await import('./dynamic-module.js'); module.run(); }; loadModule();
七、总结:一句话选择建议
- 新项目/前端项目 → 用 ESM(
import
/export
)。 - 旧项目/依赖 CommonJS 的库 → 用 CommonJS(
require
/module.exports
)。 - Node.js 中混合使用 → ESM 导入 CommonJS(
.cjs
文件),或配置type: "module"
。
通过理解两者的核心差异和适用场景,你可以更高效地选择模块系统,避免开发中的"语法坑"! 🚀