理解 Node.js 中 CommonJS 模块化的实现源码需要深入其核心机制。以下是基于 Node.js 源码的详细解析,涵盖关键模块和函数:
核心文件
lib/internal/modules/cjs/loader.js
CommonJS 模块系统的核心实现。lib/internal/modules/run_main.js
处理入口模块的执行。lib/internal/modules/package_json_reader.js
读取package.json
的"main"
字段。
关键概念与源码解析
1. Module
类:模块的抽象表示
javascript
// lib/internal/modules/cjs/loader.js
class Module {
constructor(id = "", parent) {
this.id = id; // 模块唯一标识(通常是绝对路径)
this.path = path.dirname(id); // 模块所在目录
this.exports = {}; // 模块导出的对象
this.parent = parent; // 父模块(引入者)
this.filename = null; // 模块文件绝对路径
this.loaded = false; // 是否已加载完成
this.children = []; // 子模块(该模块引入的模块)
}
// 加载模块的核心方法
load(filename) {
this.filename = filename;
// 获取文件扩展名(.js, .json, .node)
const extension = findLongestRegisteredExtension(filename);
// 根据扩展名调用对应加载器
Module._extensions[extension](this, filename);
this.loaded = true;
}
// 编译模块源码
_compile(content, filename) {
// 1. 将模块代码包裹在函数中
const compiledWrapper = wrapSafe(filename, content);
// 2. 准备参数:exports, require, module, __filename, __dirname
const dirname = path.dirname(filename);
const require = makeRequireFunction(this);
const exports = this.exports;
const thisValue = exports;
// 3. 执行包裹函数
compiledWrapper.call(
thisValue,
exports,
require,
this,
filename,
dirname
);
}
}
2. 模块包装函数
源码通过函数包裹模块代码,注入作用域变量:
javascript
// lib/internal/modules/cjs/loader.js
const wrapper = [
"(function (exports, require, module, __filename, __dirname) { ",
"\n});"
];
function wrapSafe(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
}
最终执行的代码形式:
javascript
(function (exports, require, module, __filename, __dirname) {
// 用户模块代码
});
3. require()
函数的实现
javascript
// lib/internal/modules/cjs/loader.js
Module.prototype.require = function (id) {
// 1. 解析模块路径
const filename = Module._resolveFilename(id, this);
// 2. 检查缓存
const cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// 3. 创建新模块实例
const module = new Module(filename, this);
// 4. 缓存模块(防止重复加载)
Module._cache[filename] = module;
// 5. 加载模块
try {
module.load(filename);
} catch (err) {
delete Module._cache[filename]; // 加载失败移除缓存
throw err;
}
// 6. 返回模块的 exports 对象
return module.exports;
};
4. 路径解析算法
javascript
// lib/internal/modules/cjs/loader.js
Module._resolveFilename = function (request, parent) {
// 1. 检查核心模块(如 'fs')
if (NativeModule.canBeRequiredByUsers(request)) {
return request;
}
// 2. 解析相对/绝对路径
let filename;
if (path.isAbsolute(request)) {
filename = path.resolve(request);
} else {
// 从父模块目录向上递归查找 node_modules
filename = resolveLookupPaths(request, parent);
}
// 3. 检查文件扩展名(.js, .json, .node)
filename = tryExtensions(filename);
return filename;
};
关键辅助函数:
resolveLookupPaths()
:递归查找node_modules
。tryExtensions()
:尝试添加扩展名(['.js', '.json', '.node']
)。
5. 文件加载器(Module._extensions
)
javascript
// lib/internal/modules/cjs/loader.js
Module._extensions = {
'.js'(module, filename) {
// 1. 读取文件内容
const content = fs.readFileSync(filename, 'utf8');
// 2. 编译执行
module._compile(content, filename);
},
'.json'(module, filename) {
// 直接解析 JSON 文件
const content = fs.readFileSync(filename, 'utf8');
module.exports = JSON.parse(stripBOM(content));
},
'.node'(module, filename) {
// 加载 C++ 插件
return process.dlopen(module, filename);
}
};
6. 循环依赖的处理
当模块 A 加载 B,B 又加载 A 时:
javascript
// 模块 A: a.js
exports.a = 1; // ✅ 此时已导出
const b = require('./b');
console.log(b); // { b: 2 }
exports.a = 3; // ✅ 更新导出
// 模块 B: b.js
exports.b = 2;
const a = require('./a');
console.log(a); // { a: 1 } ❗ 此时 a 尚未完全加载
exports.b = 4;
源码实现关键:
- 模块在
load()
前就被加入缓存(Module._cache
)。 - 当 B 加载 A 时,A 的
exports
对象已存在(但未完全初始化),B 获取到部分导出的对象。
核心流程总结
-
require(id)
调用用户代码触发
require()
。 -
路径解析
Module._resolveFilename()
将id
转为绝对路径。 -
缓存检查
若模块已缓存,直接返回
cachedModule.exports
。 -
创建模块实例
new Module(filename, parent)
。 -
缓存模块
Module._cache[filename] = module
。 -
加载模块
module.load(filename)
:- 根据扩展名调用对应加载器(
.js
/.json
/.node
)。 - 对
.js
文件:读取源码 → 包裹函数 → 编译执行。
- 根据扩展名调用对应加载器(
-
执行模块代码
包裹函数注入
exports, require, module, __filename, __dirname
。 -
返回
exports
对象用户得到模块导出的内容。
关键设计亮点
- 缓存机制
Module._cache
对象存储所有加载过的模块,避免重复加载。 - 同步加载
fs.readFileSync()
确保模块按顺序加载。 - 作用域隔离
包裹函数创建闭包,避免全局污染。 - 扩展名支持
通过Module._extensions
支持多种文件类型。 - 循环依赖处理
提前缓存部分导出的对象(未初始化状态)。