深入Node.js模块化:CommonJS和ESModule的全面指南

235 阅读4分钟

Node.js 主要支持两套模块化规范: CommonJS (CJS):Node.js 原生的模块系统。 ESModule (ESM):ECMAScript 标准定义的模块系统,在现代 JavaScript 中得到广泛支持。

1. CommonJS

基础用法

  1. 导入模块:使用 require('module/path') 函数。
  2. 导出模块
    • module.exports = ...:直接定义整个模块的导出值(推荐)。
    • exports.key = value:在 exports 对象上添加属性。本质exportsmodule.exports 初始值的引用。直接覆盖 exports (exports = ...) 会切断引用,导致导出失败。
  3. 适用环境:原生适用于 Node.js 环境。
  4. 启用方式
    • 默认行为(.js 文件)。
    • package.json 中明确设置 "type": "commonjs"

特性

  • 同步加载:模块在 require() 调用时运行时加载、执行并缓存结果。后续 require() 返回缓存。
  • 动态性require() 可出现在代码任何位置(非仅顶层),路径可以是动态表达式。
  • 值拷贝(引用类型为共享引用):导入的是模块导出对象的一个副本(对于原始值是拷贝,对于对象是共享引用)。导入方可以修改引用类型属性的值(影响其他导入者)。
  • 顶层 this:指向 module.exports 对象(严格模式下是 undefined 的变通实现)。
  • 循环依赖处理:Node.js 能处理循环依赖,但需注意模块在未完全执行时就被引用可能导致未初始化状态。

2. ESModule

基础用法

  1. 导入模块
    • 命名导入:import { func } from './module.mjs';
    • 默认导入:import DefaultExport from './module.mjs';
    • 命名空间导入:import * as Namespace from './module.mjs';
    • 动态导入(异步):const module = await import('./module.mjs');
  2. 导出模块
    • 命名导出:export const value = 42;export { value1, func };
    • 默认导出:export default function() {...};(每个模块仅限一个 export default)。
  3. 适用环境:现代浏览器、Node.js(v12+ 稳定支持)、支持 ESM 的构建工具(Webpack, Rollup, Vite 等)。
  4. **启用方式:
    • 使用 .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.exportsundefined (严格模式)
动态性支持动态路径和条件导入仅顶层静态 import(动态用 import()
循环依赖处理支持,但需注意未初始化状态支持,设计上更清晰
JSON 导入require() 直接支持assert { type: 'json' }
文件扩展名 (Node).js (默认或 "type": "commonjs").mjs.js (with "type": "module")

四、总结

  1. Node.js 环境选择
    • 新项目或库优先使用 ESM ("type": "module"),拥抱标准和未来。
    • 维护旧项目或需要与大量 CJS 生态兼容时使用 CJS。
  2. 互操作性
    • 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)。
  3. 双模式包
    • 库作者可通过 package.json"exports" 字段和文件扩展名 (.cjs, .mjs) 同时提供 CJS 和 ESM 入口点,兼容不同环境。
    • package.json 中明确指定 "type" ("commonjs""module") 决定 .js 文件的默认模块类型。
  4. 工具链:构建工具(Webpack, Rollup, Vite)通常能无缝处理 CJS 和 ESM 的转换与打包。