Node.js 支持两种主要的模块系统:CommonJS(CJS)和 ES6 模块(ESM)。理解它们的设计理念、语法差异以及正确的使用场景,对于构建现代、高效的 Node.js 应用至关重要。

一、核心差异对比

首先,我们可以通过一个表格来清晰地对比两者的主要区别:

特性 CommonJS (CJS) ES6 模块 (ESM) 说明与影响
设计初衷 为服务器端(Node.js)设计 为浏览器和服务器端统一设计 ESM 旨在成为 JavaScript 的官方模块标准 。
加载时机 运行时加载(动态) 编译时加载(静态) CJS 可以条件性导入,ESM 的 import 必须在顶层,利于优化(如 Tree Shaking)。
加载行为 同步加载 异步加载 CJS 在 Node.js 中同步读取模块文件,ESM 支持异步,更适合浏览器环境 。
导出方式 module.exportsexports 对象 exportexport default 语句 语法本质不同,不可混用 。
导入方式 require() 函数 import 语句 require 是函数调用,import 是声明语句 。
输出本质 值的拷贝(对基本类型) 值的引用(只读) CJS 输出的是缓存副本,ESM 输出的是动态绑定的引用 。
顶层 this 指向当前模块 指向 undefined 安全性差异。
文件扩展名 .js.cjs .mjs 或在 package.json 中设置 “type”: “module” Node.js 通过扩展名或配置识别模块类型 。
循环依赖处理 支持,但可能得到不完整的模块 支持,引用是动态绑定的 ESM 处理循环依赖更符合预期 。

二、语法与正确使用方式

1. CommonJS 模块系统

CommonJS 是 Node.js 长期以来的默认模块系统,其核心是 module.exportsrequire()

导出模块:

// math-cjs.js
// 方式1:导出一个对象 
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
module.exports = { add, subtract };

// 方式2:逐个添加属性到 exports 对象 
exports.add = add;
exports.subtract = subtract;

// 方式3:导出一个类 
class Calculator {
  multiply(a, b) { return a * b; }
}
module.exports = Calculator;

导入模块:

// app-cjs.js
// 方式1:导入整个模块对象
const math = require('./math-cjs');
console.log(math.add(5, 3)); // 输出: 8 

// 方式2:解构导入(推荐,清晰)
const { add, subtract } = require('./math-cjs');
console.log(subtract(5, 3)); // 输出: 2 

// 方式3:导入类
const Calc = require('./math-cjs'); // 假设导出的是Calculator类
const calc = new Calc();
console.log(calc.multiply(5, 3)); // 输出: 15

关键点:

  • require() 可以出现在代码的任何位置(包括条件判断中),因为是运行时同步执行 。
  • 模块第一次被 require 后会被缓存,后续调用 require 会直接返回缓存结果 。

2. ES6 模块系统

ES6 模块是 ECMAScript 标准,需要通过特定方式在 Node.js 中启用。

启用 ESM:
有两种方式:

  1. 文件扩展名法:将模块文件后缀命名为 .mjs
  2. 项目配置法:在项目的 package.json 文件中添加 "type": "module" 字段,则所有 .js 文件都将被视为 ESM 。

导出模块:

// math-esm.mjs (或 .js 在 type: module 项目中)
// 命名导出 (Named Export) 
export const PI = 3.14159;
export function multiply(a, b) { return a * b; }

// 默认导出 (Default Export) 
class Geometry {
  circleArea(r) { return PI * r * r; }
}
export default Geometry;

// 也可以先定义,后统一导出
const secret = 42;
function divide(a, b) { return a / b; }
export { secret, divide };

导入模块:

// app-esm.mjs (或 .js 在 type: module 项目中)
// 导入命名导出
import { multiply, PI } from './math-esm.mjs';
console.log(multiply(PI, 2)); // 输出: 6.28318 

// 导入默认导出
import Geometry from './math-esm.mjs';
const geo = new Geometry();
console.log(geo.circleArea(2)); // 输出: 12.56636

// 导入所有命名导出到一个命名空间对象
import * as mathUtil from './math-esm.mjs';
console.log(mathUtil.secret); // 输出: 42

// 动态导入 (返回一个Promise) 
async function loadModule() {
  const module = await import('./math-esm.mjs');
  console.log(module.divide(10, 2)); // 输出: 5
}
loadModule();

关键点:

  • import 语句必须出现在模块的顶层作用域,不能嵌套在条件语句中(动态 import() 函数除外),这是静态分析的基础 。
  • ESM 模块中不能使用 require, exports, module.exports, __filename, __dirname 等 CJS 全局变量。如需 __dirname,可用 import.meta.url 构造 。

三、互操作性(混合使用)

在 Node.js 项目中,两种模块可能共存。Node.js 提供了有限的互操作支持,但规则需要严格遵守。

  1. 在 ESM 中导入 CJS 模块:

    • 大多数情况下,ESM 可以 import CJS 模块。
    • CJS 模块的 module.exports 会被当作 ESM 的默认导出(default export)。
    // esm-app.mjs
    import cjsModule from './cjs-module.js'; // 正确,导入的是整个module.exports
    // import { named } from './cjs-module.js'; // 错误!除非CJS模块使用了特殊编译工具
    console.log(cjsModule);
    
  2. 在 CJS 中导入 ESM 模块(限制):

    • CommonJS 模块不能使用 require() 来加载 ESM 模块
    • 唯一的方式是使用 动态 import(),它会返回一个 Promise 。
    // cjs-app.js
    async function main() {
      // 使用动态 import() 导入 ESM 模块
      const esmModule = await import('./esm-module.mjs');
      console.log(esmModule.default); // 访问默认导出
      console.log(esmModule.namedExport); // 访问命名导出
    }
    main();
    

四、应用场景与最佳实践建议

场景 推荐模块系统 理由
新的 Node.js 项目 ES6 模块 (ESM) 这是 JavaScript 的语言标准,代表未来方向,支持静态分析和 Tree Shaking,有助于构建更优的应用 。
维护遗留 Node.js 项目 CommonJS (CJS) 保持一致性,避免不必要的重构风险和兼容性问题。
开发供他人使用的库 提供双模式 package.json 中使用 “exports” 字段分别指定 . (import) 和 . (require) 的入口点,以同时支持两种用户。
需要条件加载或插件化 CommonJS (CJS) 其动态 require() 特性在此类场景下更灵活 。
前端构建工具链项目 ES6 模块 (ESM) 与 Vite、Webpack、Rollup 等现代构建工具生态结合更紧密,利于 Tree Shaking 优化 。

最佳实践总结:

  1. 明确声明:在项目根目录的 package.json 中明确设置 “type”: “module”“type”: “commonjs”,或统一使用 .mjs/.cjs 扩展名来避免歧义 。
  2. 避免混用:在同一个项目中,尽量统一使用一种模块系统,以降低复杂度和维护成本。
  3. 理解差异:深刻理解“动态加载 vs 静态加载”、“值拷贝 vs 值引用”等核心差异,这会影响模块间的数据交互和状态管理 。
  4. 工具辅助:使用 ESLint、TypeScript 等工具可以帮助检测和避免模块混用导致的错误。

选择哪种模块系统取决于项目类型、团队约定和长期维护计划。对于全新的项目,拥抱 ES6 模块是更面向未来的选择 。


参考来源

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐