实现Node的require方法
手写实现Node.js模块加载机制中的require方法,理解CommonJS模块系统的工作原理
问题
Node.js 使用 CommonJS 规范来实现模块化,其中 require 是加载模块的方法。这道题要求我们手动实现一个简化版的 require 方法,理解模块的加载、缓存、执行等机制。
主要需要解决以下问题:
- 如何读取并执行模块文件
- 如何实现模块缓存机制
- 如何处理模块的导出(exports 和 module.exports)
- 如何解析模块路径
解答
const fs = require('fs');
const path = require('path');
const vm = require('vm');
class MyModule {
constructor(id) {
this.id = id; // 模块的绝对路径
this.exports = {}; // 模块导出的内容
this.loaded = false; // 模块是否已加载
}
}
// 模块缓存对象
MyModule._cache = {};
// 模块扩展名处理策略
MyModule._extensions = {
'.js'(module) {
// 读取文件内容
const content = fs.readFileSync(module.id, 'utf8');
// 编译并执行模块
module._compile(content);
},
'.json'(module) {
// 读取并解析 JSON 文件
const content = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(content);
}
};
// 编译执行模块代码
MyModule.prototype._compile = function(content) {
// 包装模块代码,注入 require、module、exports 等变量
const wrapper = [
'(function(require, module, exports, __dirname, __filename) {',
'\n});'
];
const wrappedContent = wrapper[0] + content + wrapper[1];
// 使用 vm 模块执行代码
const compiledWrapper = vm.runInThisContext(wrappedContent);
// 准备参数
const dirname = path.dirname(this.id);
const require = createRequire(this.id);
// 执行模块代码
compiledWrapper.call(
this.exports,
require,
this,
this.exports,
dirname,
this.id
);
this.loaded = true;
};
// 解析模块路径
function resolveFilename(request, parent) {
// 获取绝对路径
const filename = path.isAbsolute(request)
? request
: path.resolve(path.dirname(parent), request);
// 如果文件存在,直接返回
if (fs.existsSync(filename)) {
return filename;
}
// 尝试添加扩展名
const extensions = Object.keys(MyModule._extensions);
for (let ext of extensions) {
const filenameWithExt = filename + ext;
if (fs.existsSync(filenameWithExt)) {
return filenameWithExt;
}
}
throw new Error(`Cannot find module '${request}'`);
}
// 加载模块
function loadModule(filename) {
// 获取文件扩展名
const extname = path.extname(filename);
// 根据扩展名选择加载策略
const extension = MyModule._extensions[extname] || MyModule._extensions['.js'];
// 创建模块对象
const module = new MyModule(filename);
// 缓存模块
MyModule._cache[filename] = module;
// 加载模块
extension(module);
return module;
}
// 创建 require 函数
function createRequire(parent) {
function myRequire(request) {
// 解析模块路径
const filename = resolveFilename(request, parent);
// 检查缓存
if (MyModule._cache[filename]) {
return MyModule._cache[filename].exports;
}
// 加载模块
const module = loadModule(filename);
// 返回模块的 exports
return module.exports;
}
return myRequire;
}
// 导出主 require 函数
module.exports = createRequire(process.cwd() + '/index.js');
使用示例
// 创建测试文件 math.js
// math.js
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
// 创建测试文件 user.js
// user.js
exports.name = 'John';
exports.age = 25;
exports.greet = function() {
return `Hello, I'm ${this.name}`;
};
// 创建测试文件 config.json
// config.json
{
"port": 3000,
"host": "localhost"
}
// 使用自定义的 require
const myRequire = require('./myRequire');
// 加载 JS 模块
const math = myRequire('./math.js');
console.log(math.add(2, 3)); // 输出: 5
// 加载导出对象
const user = myRequire('./user.js');
console.log(user.name); // 输出: John
console.log(user.greet()); // 输出: Hello, I'm John
// 加载 JSON 文件
const config = myRequire('./config.json');
console.log(config.port); // 输出: 3000
// 测试缓存机制
const math2 = myRequire('./math.js');
console.log(math === math2); // 输出: true (同一个对象)
关键点
-
模块包装:使用函数包装器将模块代码包裹起来,注入
require、module、exports、__dirname、__filename等变量,实现模块作用域隔离 -
缓存机制:使用
_cache对象缓存已加载的模块,避免重复加载和执行,提高性能并保证模块单例 -
路径解析:实现
resolveFilename方法,支持相对路径、绝对路径,并自动补全文件扩展名(.js、.json) -
vm 模块:使用 Node.js 的
vm.runInThisContext方法在当前上下文中执行代码,将字符串转换为可执行函数 -
扩展名策略:通过
_extensions对象实现不同文件类型的加载策略,.js 文件需要编译执行,.json 文件直接解析 -
exports 与 module.exports:模块初始时
exports指向module.exports,最终导出的是module.exports的值,这解释了为什么直接给exports赋值不会生效 -
循环依赖处理:通过在执行前就将模块放入缓存,可以处理简单的循环依赖场景(虽然示例中未详细展示)
目录