Node.js 主要支持两套模块化规范: CommonJS (CJS):Node.js 原生的模块系统。 ESModule (ESM):ECMAScript 标准定义的模块系统,在现代 JavaScript 中得到广泛支持。
1. CommonJS
基础用法
- 导入模块:使用
require('module/path')函数。 - 导出模块:
module.exports = ...:直接定义整个模块的导出值(推荐)。exports.key = value:在exports对象上添加属性。本质:exports是module.exports初始值的引用。直接覆盖exports(exports = ...) 会切断引用,导致导出失败。
- 适用环境:原生适用于 Node.js 环境。
- 启用方式:
- 默认行为(
.js文件)。 - 在
package.json中明确设置"type": "commonjs"。
- 默认行为(
特性
- 同步加载:模块在
require()调用时运行时加载、执行并缓存结果。后续require()返回缓存。 - 动态性:
require()可出现在代码任何位置(非仅顶层),路径可以是动态表达式。 - 值拷贝(引用类型为共享引用):导入的是模块导出对象的一个副本(对于原始值是拷贝,对于对象是共享引用)。导入方可以修改引用类型属性的值(影响其他导入者)。
- 顶层
this:指向module.exports对象(严格模式下是undefined的变通实现)。 - 循环依赖处理:Node.js 能处理循环依赖,但需注意模块在未完全执行时就被引用可能导致未初始化状态。
2. ESModule
基础用法
- 导入模块:
- 命名导入:
import { func } from './module.mjs'; - 默认导入:
import DefaultExport from './module.mjs'; - 命名空间导入:
import * as Namespace from './module.mjs'; - 动态导入(异步):
const module = await import('./module.mjs');
- 命名导入:
- 导出模块:
- 命名导出:
export const value = 42;或export { value1, func }; - 默认导出:
export default function() {...};(每个模块仅限一个export default)。
- 命名导出:
- 适用环境:现代浏览器、Node.js(v12+ 稳定支持)、支持 ESM 的构建工具(Webpack, Rollup, Vite 等)。
- **启用方式:
- 使用
.mjs文件扩展名。 - 在
package.json中设置"type": "module"(此时.js文件被视为 ESM)。 - 在 CLI 调用时使用
--input-type=module标志。
- 使用
特性
- 编译时静态分析 / 异步加载:
import语句在编译时解析依赖关系,支持异步加载(尤其适用于浏览器) - 静态结构:顶级
import/export必须在模块顶层,路径必须是静态字符串字面量(动态导入import()除外) - 绑定(只读引用):导入的是原始导出标识符的只读实时绑定(live read-only binding)。修改原始导出模块中的值,导入方的绑定值也会更新;但导入方不能直接修改绑定的值(
import的对象属性可修改,但这违背 ESM 只读绑定原则) - 顶层
this:严格模式下指向undefined - JSON 导入:需要使用
Assertion语法:import data from './data.json' assert { type: 'json' };
3. CommonJS 与 ESModule 的区别
| 特性 | CommonJS (CJS) | ESModule (ESM) |
|---|---|---|
| 加载时机与方式 | 运行时同步加载 | 编译时静态分析 / 异步加载 |
| 导入语句 | require() | import / import() |
| 导出语句 | module.exports / exports.* | export / export default |
| 导出值特性 | 值拷贝(对象属性可修改) | 只读实时绑定(对象属性可修改但不推荐) |
模块顶层的 this | 指向 module.exports | undefined (严格模式) |
| 动态性 | 支持动态路径和条件导入 | 仅顶层静态 import(动态用 import()) |
| 循环依赖处理 | 支持,但需注意未初始化状态 | 支持,设计上更清晰 |
| JSON 导入 | require() 直接支持 | 需 assert { type: 'json' } |
| 文件扩展名 (Node) | .js (默认或 "type": "commonjs") | .mjs 或 .js (with "type": "module") |
四、总结
- Node.js 环境选择:
- 新项目或库优先使用 ESM (
"type": "module"),拥抱标准和未来。 - 维护旧项目或需要与大量 CJS 生态兼容时使用 CJS。
- 新项目或库优先使用 ESM (
- 互操作性:
- ESM 导入 CJS:在 ESM 中,可以使用
import导入 CJS 模块。CJS 模块的module.exports会被视为 ESM 的默认导出 (default)。命名导出可能需通过静态分析推断或使用import * as cjsModule from 'cjs-module'访问其属性。 - CJS 导入 ESM:不支持在 CJS 中使用
require()直接加载 ESM 模块(会报错)。必须使用动态导入import('esm-module')(返回 Promise)。
- ESM 导入 CJS:在 ESM 中,可以使用
- 双模式包:
- 库作者可通过
package.json的"exports"字段和文件扩展名 (.cjs,.mjs) 同时提供 CJS 和 ESM 入口点,兼容不同环境。 - 在
package.json中明确指定"type"("commonjs"或"module") 决定.js文件的默认模块类型。
- 库作者可通过
- 工具链:构建工具(Webpack, Rollup, Vite)通常能无缝处理 CJS 和 ESM 的转换与打包。