commonjs的模块原理及实现

361 阅读2分钟

commonjs规范

  1. 每个js文件都是一个模块
  2. 模块的导出 module.exports
  3. 模块的导入require

require的执行步骤

  1. 调用require时会内部调用Module._load方法;

  2. Module.resolveFilename 解析文件名,这一步会尝试给文件加上后缀;

    const filename = Module._resolveFilename(request, parent, isMain);
    
  3. 创建当前模块实例,new Module() => {id,exports};

    const module = cachedModule || new Module(filename, parent);
    function Module(id = '', parent) {
      this.id = id;
      this.path = path.dirname(id);
      this.exports = {};
      moduleParentCache.set(this, parent);
      updateChildren(parent, this, false);
      this.filename = null;
      this.loaded = false;
      this.children = [];
    }
    
  4. 调用module.load加载文件

    1. 根据文件后缀调用相应的方法进行加载,这里使用了策略模式;

      Module._extensions[extension](this, filename);
      
    2. 读取文件内容

      content = fs.readFileSync(filename, 'utf8');
      
    3. 调用wrapSafe方法给读取到的内容包装一个函数,老版本直接使用字符串拼接,新版本有专门的方法vm.compileFunction运行工之后直接包装成一个函数,包装完之后就长这样:

      ƒ (exports, require, module, __filename, __dirname) {\nconst str = 'abc';\nmodule.exports = str;\n\n}
      		
            // 参数如下,也就是说每个文件都可以直接用这几个变量                                               
            'exports',
            'require',
            'module',
            '__filename',
            '__dirname',
      
    4. 执行这个函数,并将module传递给用户,用户将手动给module.exports进行赋值

      result = ReflectApply(compiledWrapper, thisValue, [exports, require, module, filename, dirname]);
      return result;
      
  5. 返回module.exports;;

一个mini版的commonjs实现

const path = require('path');
const fs = require('fs');
const vm = require('vm');

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

Module._extensions = {
    '.js'(id) {
        const content = fs.readFileSync(id, 'utf-8');
        const wrapperFn = vm.compileFunction(content, ['exports', 'require', 'module', '__filename', '__dirname']);
        return wrapperFn
        
    },
    '.json'() {},
};
Module._resolveFilename = function (id) {
    let filepath = path.resolve(__dirname, id);

    if (fs.existsSync(filepath)) return filepath;

    // 取出的key的顺序与Object.keys是否一致?此处要求有顺序
    const exts = Reflect.ownKeys(Module._extensions);
    for (let i = 0; i < exts.length; i++) {
        const file = filepath + exts[i];
        if (fs.existsSync(file)) return file;
    }

    throw new Error('Module not found: ' + id);
};
Module.prototype.load = function (id) {
    const ext = path.extname(id);
    const safeWrap = Module._extensions[ext](id);

    const exports = this.exports;
    const thisValue = exports;
    const require = req;
    const module = this;
    const filename = id;
    const dirname = path.dirname(filename);
    Reflect.apply(safeWrap, thisValue, [exports, require, module, filename, dirname])
};

const req = function (id) {
    const filename = Module._resolveFilename(id);
    const module = new Module(filename);
    module.load(filename);
    return module.exports;
};

const str = req('./1');
console.log('req的输出:' + str.toString());

todo

加载过的模块进行缓存,第二次取值时直接返回