Node.js 模块系统

浏览器里分文件靠 <script src> 标签;Node 在电脑上跑,需要更严谨的"模块"机制来组织代码、复用逻辑、隔离作用域。Node 历史上有两套模块系统:传统的 CommonJS 和现代的 ESM。这一章把它们讲透。

1. 为什么需要模块?

如果把所有代码都写在一个文件里,项目大了根本没法维护。模块化的好处:

2. CommonJS:Node 的传统默认

Node 最初(2009 年)用的就是 CommonJS——那时 ESM 标准还没出来。它通过 require 引入、module.exports 导出:

// CommonJS 是 Node 的传统默认模块系统
// 文件名通常用 .js 或 .cjs

// ---- math.js ----(导出)
function add(a, b) {
  return a + b;
}
const PI = 3.14;

// 方式 1:整体导出(对象)
module.exports = { add, PI };

// 方式 2:逐个挂载(效果一样)
// exports.add = add;
// exports.PI = PI;

// ---- app.js ----(引入)
const { add, PI } = require("./math");
console.log(add(2, 3));   // 5
console.log(PI);          // 3.14

关键点:

3. ESM:现代推荐

ES6(2015)制定了官方的 ESM 标准,用 import / export。它设计上静态化——依赖关系在编译期就能确定,利于 tree-shaking(打包时删掉没用到的代码)。Node 从 13.2 开始正式支持 ESM,今天已是推荐写法:

// ESM(ECMAScript Modules)是现代推荐写法
// 文件名用 .mjs,或在 package.json 设 "type": "module"

// ---- math.mjs ----(导出)
export function add(a, b) {
  return a + b;
}
export const PI = 3.14;

// 默认导出(一个文件只能有一个 default)
export default function multiply(a, b) {
  return a * b;
}

// ---- app.mjs ----(引入)
import { add, PI } from "./math.mjs";
import multiply from "./math.mjs";    // 默认导出,名字自定义
import * as math from "./math.mjs";    // 全部导入

console.log(add(2, 3));     // 5
console.log(multiply(4, 5)); // 20

ESM 与 CommonJS 的主要区别:

4. 三种模块来源

// 引入内置模块:直接写名字,不加路径
const fs = require("fs");              // 文件系统
const path = require("path");          // 路径处理
const http = require("http");          // HTTP 服务
const os = require("os");              // 操作系统信息
const crypto = require("crypto");      // 加密
const url = require("url");            // URL 解析

// 第三方模块:先 npm install,再 require 包名
const express = require("express");    // 需要 npm install express
const lodash = require("lodash");      // 需要 npm install lodash

// 自己的模块:必须加 ./ 或 ../ 前缀
const myTool = require("./utils/tool"); // 同目录
const config = require("../config");     // 上一级目录

记住这个区分:内置模块(fspathhttp...)直接写名字;第三方模块要先 npm install 再写名字;自己的模块必须带 ./../ 前缀。漏掉前缀是新手常见错误——Node 会去 node_modules 里找一个叫 utils 的包,找不到就报错。

5. package.json 的 type 字段

Node 怎么知道一个 .js 文件该用 CommonJS 还是 ESM?看 package.jsontype 字段:

// package.json 的 type 字段决定 .js 文件按哪种模块处理
{
  "name": "my-app",
  "version": "1.0.0",
  "type": "module",        // 设为 module 则 .js 都按 ESM 处理
  "main": "index.js"
}

// 不设 type 或设 "commonjs" 则 .js 按 CommonJS 处理(默认)
// 强制某文件用某系统:改后缀
//   .cjs → 强制 CommonJS
//   .mjs → 强制 ESM
//   .js  → 看 package.json 的 type

简单结论:

6. 两种系统互相引用

ESM 里可以引入 CommonJS 模块(默认导出当默认导入),但不能用命名导入解构 CJS 的属性:

// 旧的 CJS 包(old-lib.cjs)
module.exports = { foo: 1, bar: 2 };

// ESM 里引入它(app.mjs)
import oldLib from "./old-lib.cjs";   // 默认导入整个对象,正确
console.log(oldLib.foo);              // 1

// 但命名导入会报错(CJS 是运行时对象,静态分析不出来)
import { foo } from "./old-lib.cjs";  // 报错

反过来,CommonJS 里引入 ESM 要用动态 import()(因为 ESM 是异步加载的):

// ESM 模块(new-lib.mjs)
export const hello = "hi";

// CommonJS 里引入它(app.cjs)——只能用动态 import
async function main() {
  const mod = await import("./new-lib.mjs");
  console.log(mod.hello);   // hi
}
main();

7. 循环依赖

当 A 引用了 B,B 又引用了 A,就形成循环依赖。Node 不会报错,但行为很微妙:

// a.js
const b = require("./b");
console.log("a 里看到 b:", b.x);   // 此时 b.x 可能是 undefined
module.exports = { x: "from-a" };

// b.js
const a = require("./a");
console.log("b 里看到 a:", a.x);   // undefined(a 还没执行完)
module.exports = { x: "from-b" };

// 运行 node a.js 的输出:
// b 里看到 a: undefined
// a 里看到 b: from-b

原因:require 是同步执行的,Node 检测到循环时会返回"当前已导出的部分"(可能是个空对象)。最佳实践是从设计上避免循环依赖——把公共逻辑抽到第三个模块,让 A 和 B 都依赖 C,而不是互相依赖。

8. 加载目录

// 加载目录时,Node 会自动找该目录下的 index.js
// 假设有这样的结构:
//   utils/
//     index.js     ← 默认入口
//     format.js
//     validate.js

// 三种 require 都能命中 utils/index.js:
const utils = require("./utils");
const utils = require("./utils/");
const utils = require("./utils/index");

// 但建议显式写 ./utils/index,清晰明确

这个机制让"一个目录对外是一个模块"成为可能,大型项目常用它来组织子系统。也可以在目录下放 package.json 指定 main 字段,效果一样。

小结

Node 的模块系统是组织代码的地基。记住三点:CommonJS 用 require/module.exports,ESM 用 import/export;新项目选 ESM,读老代码懂 CommonJS;内置模块直接写名字,自定义模块带前缀。下一篇我们用内置的 http 模块,几行代码起一个 Web 服务器。

← 上一篇 Node.js 环境安装

下一篇 Node.js HTTP 服务

✈️💬