Node.js 采用模块化设计,每个文件都是一个独立的模块。模块系统是 Node.js 的基石,它允许你将代码拆分成可复用的单元,并通过 require 和 module.exports 进行交互。本章将全面解析 Node.js 的模块机制,包括 CommonJS 和 ES Modules。
在复杂的应用程序中,将代码分割成多个文件有助于:
Node.js 最初采用 CommonJS 模块规范,后来也逐渐支持 ECMAScript 模块(ESM)。
CommonJS 是 Node.js 默认的模块系统(直到 ESM 出现)。每个文件都被视为一个模块,模块内的变量、函数、类都是私有的,除非显式导出。
每个模块都有一个 module 对象,其中包含 exports 属性。初始时 module.exports 是一个空对象。你可以通过两种方式导出:
module.exports 赋值(可以导出任意值:对象、函数、字符串等)。exports 快捷方式(exports 是 module.exports 的引用)。示例: 创建 math.js
// 方式一:给 exports 添加属性
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
// 方式二:直接赋值给 module.exports(会覆盖 exports)
// module.exports = {
// add: (a, b) => a + b,
// subtract: (a, b) => a - b
// };
exports 和 module.exports,因为 exports 只是对 module.exports 的引用,一旦 module.exports 被重新赋值,exports 就不再指向同一个对象。
使用 require() 函数加载其他模块。它返回目标模块的 module.exports 的值。
// app.js
const math = require('./math.js');
console.log(math.add(5, 3)); // 8
require() 的参数可以是:
require('fs')、require('http')。./、/ 或 ../ 开头的路径,指向具体的文件或目录。require('express'),从 node_modules 中查找。当调用 require() 时,Node.js 按照以下步骤解析模块:
fs、path),如果是则直接返回。./、/ 或 ../ 开头,则将其解析为绝对路径,并加载对应文件。如果找不到文件,会尝试添加 .js、.json、.node 扩展名。package.json 文件,并读取其 main 字段指定的入口文件;如果没有 package.json,则默认加载 index.js、index.json、index.node。node_modules 目录,直到根目录。// 假设项目结构
// project/
// ├── node_modules/
// │ └── lodash/
// ├── lib/
// │ └── util.js
// └── app.js
// 在 app.js 中:
const _ = require('lodash'); // 从 node_modules 查找
const util = require('./lib/util'); // 文件模块
模块在第一次加载后会被缓存,后续 require() 直接返回缓存的结果,不会重新执行模块代码。可以通过 console.log(require.cache) 查看缓存对象。如果希望重新加载模块,可以删除缓存:
delete require.cache[require.resolve('./math.js')];
const math = require('./math.js'); // 重新加载
当两个模块相互引用时,Node.js 会返回当前已经执行的 exports 部分(未完成的模块将获得一个不完整的对象)。例如:
a.js
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('a 中,b.done =', b.done);
exports.done = true;
console.log('a 结束');
b.js
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('b 中,a.done =', a.done);
exports.done = true;
console.log('b 结束');
运行 node a.js 输出:
a 开始
b 开始
b 中,a.done = false
b 结束
a 中,b.done = true
a 结束
可见 Node.js 能够处理循环依赖,但设计时应尽量避免。
从 Node.js 12 开始,ES Modules 得到了稳定支持。ESM 使用 import 和 export 语句,是 JavaScript 官方的模块标准。
有两种方式告诉 Node.js 将文件视为 ES 模块:
.mjs。package.json 中设置 "type": "module",此时 .js 文件会被视为 ESM(但 .cjs 仍为 CommonJS)。在 ESM 中,不能直接使用 require()、__dirname、__filename 等 CommonJS 特有的变量。如果需要获取当前目录,可以使用 import.meta.url 结合 url 模块:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
命名导出:
// lib.js
export const name = 'ES Module';
export function greet() {
return 'Hello';
}
// 或者统一导出
const age = 5;
export { age };
默认导出:
// default.js
export default function() {
console.log('default export');
}
导入:
import { name, greet, age } from './lib.js';
import myDefault from './default.js';
在 ESM 中,可以使用 import 加载 CommonJS 模块(只能默认导入)。例如:
import fs from 'fs'; // 内置模块是 CommonJS,但可以默认导入
import _ from 'lodash'; // 第三方 CommonJS 模块
在 CommonJS 中,不能使用 require() 加载 ESM(因为 ESM 是异步的)。如果需要,可以使用动态 import():
// CommonJS 中加载 ESM
(async () => {
const esModule = await import('./es-module.mjs');
})();
package.json 是 Node.js 项目的核心文件,其中 main 字段指定了模块的入口文件。对于 ESM,还可以使用 exports 字段定义更精细的导出子路径:
{
"name": "my-package",
"type": "module",
"main": "./dist/index.js",
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js"
}
}
这样用户可以通过 import 'my-package' 和 import 'my-package/utils' 分别引入。
Node.js 提供了一系列内置模块,如 fs、path、http、events 等。它们在 Node.js 源代码中编译而成,加载速度最快。可以通过 require('module') 或 import 直接使用。
const fs = require('fs');
import fs from 'fs'; // ESM 中也可以默认导入
在 CommonJS 中,Node.js 在执行模块代码之前,会将其包装在一个函数中,以实现模块作用域。这个包装函数如下:
(function(exports, require, module, __filename, __dirname) {
// 模块代码
});
这就是为什么在模块中可以直接使用 exports、require、module、__filename、__dirname 的原因。
exports 和 module.exports 到底有什么区别?exports 是 module.exports 的引用,初始指向同一个对象。如果给 exports 添加属性,会反映到 module.exports;但如果直接给 module.exports 赋值新对象,exports 就不再指向它了。
require() 是同步的?本章涵盖了 Node.js 模块系统的方方面面,包括 CommonJS 的导出/导入、加载机制、缓存、循环依赖,以及 ES Modules 的基本用法。掌握这些知识后,你将能更好地组织代码并利用 npm 生态。下一章我们将深入 npm 包管理器,学习如何安装、发布和管理依赖。