《深入浅出 Node.js》第二章:模块机制 详细总结
第二章是全书的基础章节,朴灵作者用大量篇幅剖析了 Node.js 的 CommonJS 模块系统实现细节。理解这一章,你就彻底掌握了 require 的“黑魔法”、模块缓存、循环依赖安全、核心模块加载、NPM 包管理等核心机制。即使现代 Node 支持 ESM,CommonJS 仍是主流(尤其是 npm 包),这些原理至今适用。
章节结构与每个小节详细总结 + 例子
2.1 CommonJS 规范
2.1.1 CommonJS 的出发点
- 浏览器 JS 缺乏模块系统,导致全局变量污染、依赖管理混乱。
- CommonJS(2009年)目标:为服务器端 JS 定义模块化标准(模块引用、定义、标识)。
2.1.2 CommonJS 的模块规范
- 模块引用:
var math = require('math');(同步)。 - 模块定义:
exports.add = ...或module.exports = ...(推荐复杂对象)。 - 模块标识:路径或名字。
例子1:基本导出
// math.js
exports.add = (a, b) => a + b; // 简写
module.exports = { // 等价,推荐
add: (a, b) => a + b,
sub: (a, b) => a - b
};
例子2:exports vs module.exports 区别
exports.foo = 'bar';
module.exports = { baz: 'qux' }; // 覆盖!最终导出 { baz: 'qux' }
2.2 Node 的模块实现(核心!)
Node 实现 CommonJS 的三步流程:
2.2.1 优先从缓存加载
- 所有加载过的模块缓存到
require.cache(key 为绝对路径)。 - 再次 require 直接返回同一 exports 对象(引用共享)。
例子:缓存共享 + 副作用只执行一次
// a.js
console.log('a.js 执行');
exports.done = false;
// b.js
const a = require('./a');
a.done = true;
// main.js
require('./a'); // 不打印 'a.js 执行'
require('./b');
console.log(require('./a').done); // true(共享修改)
2.2.2 路径分析和文件定位
require 参数分类:
- 核心模块(如 'fs'):优先、最快。
- 文件模块:相对/绝对路径,试 .js → .json → .node。
- 包模块:node_modules 逐级向上查找,读 package.json 的 main。
例子:路径解析顺序
require('express'); // node_modules/express → package.json main
require('./lib/utils'); // 当前目录 lib/utils.js
require('../config'); // 上级目录 config/index.js(无main默认index)
2.2.3 模块编译
- .js:读取内容,用函数包裹执行(创建独立作用域):
(function(exports, require, module, __filename, __dirname) { // 用户代码 }); - .json:JSON.parse → module.exports。
- .node:dlopen 加载 C++ addon。
例子:闭包避免污染
// test.js
var secret = '内部变量';
exports.getSecret = () => secret;
// main.js
const t = require('./test');
console.log(global.secret); // undefined(不污染全局)
2.3 核心模块
- 分两类:
- JS实现(lib/目录,如 util)。
- C++实现(src/目录,如 fs、http)。
- 加载最快(编译进二进制)。
例子:fs 是 C++ 绑定 libuv。
2.4 C/C++扩展模块
- .node 文件,用 dlopen 加载。
- 适合性能瓶颈(如图像处理)。
例子:书里 Hello World addon(V8 API)。
2.5 模块调用栈
- 自顶向下:入口文件 → 用户模块 → 核心模块。
- 缓存让循环依赖安全(先导出空对象)。
例子:循环依赖
// a.js
console.log('a 开始');
exports.done = false;
const b = require('./b');
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');
console.log('b 中,a.done =', a.done);
exports.done = true;
console.log('b 结束');
// 输出:a开始 → b开始 → b中 a.done=false → b结束 → a中 b.done=true → a结束
2.6 包与NPM
- 包 = 目录 + package.json。
- 关键字段:name、version(SemVer)、main、dependencies、scripts、bin 等。
例子:package.json
{
"name": "my-pkg",
"version": "1.2.3",
"main": "lib/index.js",
"bin": { "my-cli": "cli.js" },
"dependencies": { "lodash": "^4.17.0" }
}
NPM 解决了依赖安装、版本冲突(SemVer + ^/~)。
2.7 前后端共用模块
- 理想:同一模块前后端通用。
- 难点:require 同步 vs 浏览器异步。
- 方案:AMD(RequireJS)、CMD(SeaJS)、Browserify 打包。
- UMD 兼容写法(书里经典示例)。
例子:UMD 包裹
(function(name, definition) {
if (typeof exports === 'object') module.exports = definition(); // CommonJS
else if (typeof define === 'function' && define.amd) define(definition); // AMD
else window[name] = definition(); // 全局
})('myLib', function() { return { foo: 'bar' }; });
2.8 & 2.9 总结与参考资源
- CommonJS + Node 实现解决了 JS 模块化痛点。
- 缓存 + 包裹 + 路径解析 = 高效稳定系统。
- NPM 是生态基石,前后端共用是未来(现在已实现)。