commonjs模块的简易版本实现

184 阅读2分钟

实现简易版本的commonjs加载

带着下面的问题去读读代码吧!

  • node里面的文件就是模块?
  • 读取的js文件,是如何执行的?
  • exports module.exports的关系?
  • js里面的对象之间的引用关系?
如何理解文件就是模块?

node里面,js代码在被引入过去都会被包装成如下的函数,是不是我们只需要再我们的require函数里面去调用这个函数(当然下面的那个函数需要我们自己读取到文件内容,然后组装成下面的函数形式),解析函数字符串为可执行的js,传入引用的参数。module.exports就会被赋值。再返回module.exports,就得到了需要的模块里面的内容了


// (function(exports,module,require){
    this == module.exports // true
    module.exports = 'hello';
// })

module.exportsexports
exports = module.exports

exports.a = 'xxx' // 一个引用地址,module.exports也会被赋值
exports = 'xxx' // 这个时候exports的引用地址就错误了。就获取不到模块的内容了

参数的引用

但我们传递给函数的参数是一个引用类型数据的时候。当函数执行的时候,会先执行参数赋值的阶段,引用类型数据的话,赋值的就是一个引用地址。就会出现如下的情况

var test = {}

function fn(a){
    a.name = 'xxx'
}

fn(test)

console.log(test) // {name:'xxx'}


代码实现如下
const fs = require("fs");
const path = require("path");
const vm = require("vm");

function Module(filePath) {
  this.id = filePath;
  this.exports = {};
}

Module._cache = {};

Module.fnStr = ["(function(module,exports,req,__fileName,__dirname){\n", "})"];

Module.extensions = {
  ".js": function(module) {
    const content = fs.readFileSync(module.id, "utf8");
    const fnString = Module.fnStr[0] + content + Module.fnStr[1];
    const fn = vm.runInThisContext(fnString);
    // 文件就是模块 exports = module.exports exports其实就是指向的 module.exports
    // 函数执行后 会给传递进去的module.exports赋值。module又是一个对象,函数参数的引用,就给下面实例化的 module上赋值了。就能够获取到文件的内容了
    fn.call(
      module.exports,
      module,
      module.exports,
      req,
      module.id,
      path.dirname(module.id)
    );
  },
  ".json": function(module) {
    const jsonString = fs.readFileSync(module.id, "utf8");
    module.exports = JSON.parse(jsonString);
  }
};

Module.resolveFileName = function(filePath) {
  let absolutePath = path.resolve(__dirname, filePath);
  let flag = fs.existsSync(absolutePath); // 判断文件是否存在
  let current = absolutePath;
  if (!flag) {
    let keys = Object.keys(Module.extensions);
    for (let i = 0; i < keys.length; i++) {
      let combinePath = absolutePath + keys[i];
      let flag = fs.existsSync(combinePath); // 增加文件后缀后再判断文件是否存在
      if (flag) {
        current = combinePath;
        break;
      } else {
        current = null;
      }
    }
  }
  if (!current) {
    throw new Error(`当前文件不存在${current}`);
  }
  return current;
};

Module.prototype.load = function() {
  // this.id 就是文件路径
  let extName = path.extname(this.id);
  Module.extensions[extName](this);
};

function req(filePath) {
  let absolutePath = Module.resolveFileName(filePath); // 获取到文件的绝对路径 支持了 动态匹配.js .json后缀
  let module = new Module(absolutePath);

  if (Module._cache[absolutePath]) {
    return Module._cache[absolutePath].exports;
  }

  // 值是一个引用地址。真正有值的时候是再执行完module.load()后
  Module._cache[absolutePath] = module;
  module.load(); // 读取文件
  return module.exports;
}

本人前端菜鸟一枚,欢迎阅读并指出错误!