深入浅出《Node.js》-第二章总结

21 阅读3分钟

《深入浅出 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 参数分类:

  1. 核心模块(如 'fs'):优先、最快。
  2. 文件模块:相对/绝对路径,试 .js → .json → .node。
  3. 包模块: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 是生态基石,前后端共用是未来(现在已实现)。