Node.js 模块系统完全指南:CommonJS 与 ESM 对比

小飞兽 Node.js 332 次阅读 2026-07-13

Node.js 模块系统概述

Node.js 支持两套模块系统:CommonJS(CJS)ES Module(ESM)。CommonJS 是 Node.js 诞生之初就使用的规范,通过 require()module.exports 实现;ES Module 是 ES6 引入的官方标准,使用 importexport 语法。

CommonJS 规范

CommonJS 是 Node.js 默认的模块系统,每个 .js 文件都被视为一个独立的模块。

// math.js
// 命名导出(多个)
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// 默认导出(只能有一个)
module.exports = {
  add,
  subtract,
};

// 同时支持命名导出和默认导出
exports.add = add;
exports.subtract = subtract;
module.exports = class Calculator { /* ... */ }; // 默认导出会覆盖上面的命名导出
// 使用 CommonJS
const { add, subtract } = require('./math');
const calc = require('./math');

// 注意:不要混用 exports.xxx 和 module.exports
// module.exports 会覆盖整个 exports 对象

ES Module 规范

ES Module 需要在 package.json 中设置 "type": "module",或使用 .mjs 扩展名。

// utils.mjs
// 命名导出(可多个)
export const name = 'Node.js';
export const version = '20.x';

// 等价写法:统一导出
const PI = 3.14159;
const E = 2.71828;
export { PI, E };

// 默认导出(每个模块只能有一个)
export default class Utils {
  static greet(name) {
    return `Hello, ${name}!`;
  }
}
// app.mjs
// 导入命名导出
import { name, version } from './utils.mjs';

// 导入默认导出
import Utils from './utils.mjs';

// 导入时重命名
import { name as programName } from './utils.mjs';

// 同时导入命名和默认
import Utils, { name } from './utils.mjs';

// 动态导入(返回 Promise)
const module = await import('./utils.mjs');

循环依赖处理

循环依赖是 Node.js 开发中的常见陷阱。以下是正确处理方式:

// a.js
console.log('a.js 开始加载');
const { b } = require('./b');

function a() {
  return '我是 A,依赖 B 的结果:' + b();
}

console.log('a.js 加载完成');
module.exports = { a };
// b.js
console.log('b.js 开始加载');
const { a } = require('./a'); // 这里拿到的是 a.js 中 require('./b') 之前的部分

function b() {
  return '我是 B,但我拿不到完整的 A';
}

console.log('b.js 加载完成');
module.exports = { b };

正确的循环依赖处理原则:尽量避免循环依赖,如果无法避免,在模块加载完成后通过事件或回调传递依赖,或将共享代码提取到第三个模块中。

package.json 中的模块类型配置

{
  "name": "my-app",
  "type": "module",  // "module" 表示使用 ESM,"commonjs"(默认)表示使用 CommonJS
  "exports": {
    ".": {
      "import": "./dist/index.mjs",    // ESM 入口
      "require": "./dist/index.cjs"    // CommonJS 入口
    },
    "./utils": "./utils/index.js"       // 子路径别名
  },
  "main": "./dist/index.cjs",           // 仅用于 CommonJS
  "module": "./dist/index.mjs"          // bundler 使用(如 webpack、rollup)
}

CommonJS 与 ESM 核心区别

| 特性 | CommonJS | ES Module |
|------|---------|-----------|
| 语法 | require() / module.exports | import / export |
| 加载方式 | 同步,运行时加载 | 异步,编译时确定 |
| 导出 | 可修改(运行时不限制) | 导入绑定不可修改(只读) |
| 循环依赖 | 支持,但不完整 | 支持,但需要小心 |
| 顶層 this | module.exports(相当于 undefined) | undefined |
| 扩展名 | .js.json.node | .mjs.js(需 package.json type=module) |

常见问题

Q1: 如何在 ESM 中导入 CommonJS 模块?

直接 import 即可,CommonJS 模块的 module.exports 会被当作 ESM 的默认导出。但命名导出(如 exports.xxx)在某些场景下不可用,需要用 import * as xxximport xxx.default

Q2: <code>import</code> 和 <code>require()</code> 能混用吗?

不能。在 ES Module 中禁止使用 require(),在 CommonJS 中也不能使用 import/export 语法(Node.js 支持通过 .mjs 文件强制使用 ESM)。建议团队统一使用一种规范。

Q3: 模块的缓存机制是怎样的?

两种模块系统都使用相同的缓存机制。模块被加载后,结果被缓存在 require.cache(CommonJS)或内部模块缓存(ESM)中。重复 requireimport 同一模块,不会重新执行模块代码,只会返回缓存结果。

延伸阅读

  • <a href="https://nodejs.org/zh-cn/docs/guides/node-core-libs/">Node.js 官方文档 - 模块系统</a>
  • <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/export">ES Module 规范文档</a>
  • <a href="https://nodejs.org/zh-cn/docs/guides/esm-nodejs-compat/">Node.js 18+ 原生 ESM 支持</a>