实现Node的require方法

手写实现Node.js模块加载机制中的require方法,理解CommonJS模块系统的工作原理

问题

Node.js 使用 CommonJS 规范来实现模块化,其中 require 是加载模块的方法。这道题要求我们手动实现一个简化版的 require 方法,理解模块的加载、缓存、执行等机制。

主要需要解决以下问题:

  1. 如何读取并执行模块文件
  2. 如何实现模块缓存机制
  3. 如何处理模块的导出(exports 和 module.exports)
  4. 如何解析模块路径

解答

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 (同一个对象)

关键点

  • 模块包装:使用函数包装器将模块代码包裹起来,注入 requiremoduleexports__dirname__filename 等变量,实现模块作用域隔离

  • 缓存机制:使用 _cache 对象缓存已加载的模块,避免重复加载和执行,提高性能并保证模块单例

  • 路径解析:实现 resolveFilename 方法,支持相对路径、绝对路径,并自动补全文件扩展名(.js、.json)

  • vm 模块:使用 Node.js 的 vm.runInThisContext 方法在当前上下文中执行代码,将字符串转换为可执行函数

  • 扩展名策略:通过 _extensions 对象实现不同文件类型的加载策略,.js 文件需要编译执行,.json 文件直接解析

  • exports 与 module.exports:模块初始时 exports 指向 module.exports,最终导出的是 module.exports 的值,这解释了为什么直接给 exports 赋值不会生效

  • 循环依赖处理:通过在执行前就将模块放入缓存,可以处理简单的循环依赖场景(虽然示例中未详细展示)