实现NodeJS里的require导入模块功能

112 阅读2分钟

我正在参加「掘金·启航计划」

最近在研究模块化的东西,看了下nodeJS的源码里的require功能,发现挺简单的,就此记录一下。

思路:我们使用require时需要传递一个相对路径。那么就需要根据这个相对路径,找出模块的绝对路径,然后根据不同文件格式,使用不同的解析方法读取该模块的内容。

实现require函数

  • 根据模块的相对路径获取绝对路径
  • 尝试从缓存中读取模块,如果没有,则使用tryModuleLoad方法加载模块。
  • 返回模块解析后的内容
// 公共模块
let path = require("path");
let fs = require("fs");
let vm = require("vm");

function myRequire(filePath) {
  // 1.将传入的相对路径转换成绝对路径
  let absPath = path.join(__dirname, filePath);
  // 2.如果缓存中有该模块,直接使用,不用重新读取
  let cachedModule = MyModule._cache[absPath];
  if (cachedModule) {
    return cachedModule.exports;
  }
  // 3.如果缓存中没有,需要进行缓存
  let module = new MyModule(absPath);
  MyModule._cache[absPath] = module;
  // 4.利用tryModuleLoad方法加载模块
  tryModuleLoad(module);
  // 5.返回模块的exports
  return module.exports
}

实现模块的缓存功能

创建一个MyModule对象,key为模块的相对路径,值为模块的内容。

class MyModule {
  constructor(path) {
    this.path = path; // 保存当前模块的绝对路径
    this.exports = {};
  }
}
// 用于缓存模块
MyModule._cache = {};

实现文件读取功能

根据不同类型的文件,执行不同的解析方式

  • 如果是json格式的代码,直接用JSON.parse进行解析,然后将解析后的模块内容赋值给module。
  • 如果是js格式的代码,用MyModule.wrapper的字符串包裹后,形成了一个函数字符串。再使用nodeJS里的vm.runInThisContext方法进行执行。当然也可以用eval或者new Function(codeStr)进行解析,只不过这两种方法会读取其他被导入模块的其他模块的作用域中的变量,影响代码执行。
  • 使用vm.runInThisContext方法执行后,生成了一个函数,并将module空对象传递进去,最终实现了module的赋值,并缓存到了MyModule._cache中。
MyModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

MyModule._extensions = {
  ".js": function (module) {
    // 1.读取JS代码
    let script = fs.readFileSync(module.path);
    // 2.将JS代码包裹到函数中
    let strScript = MyModule.wrapper[0] + script + MyModule.wrapper[1];
    // 3.将字符串转换成JS代码
    let jsScript = vm.runInThisContext(strScript);
    // 4.执行转换之后的JS代码
    // 将module作为函数的执行对象,这样就会将模块的内容赋值给module对象。
    jsScript.call(module.exports, module.exports);
  },
  ".json": function (module) {
    let json = fs.readFileSync(module.path);
    let obj = JSON.parse(json);
    module.exports = obj;
  }
};

function tryModuleLoad(module) {
  // 1.取出模块后缀
  let extName = path.extname(module.path);
  // 将module传递给模块解析函数
  MyModule._extensions[extName](module);
}

到这里就实现了一个require导入模块的功能