实现commonjs规范

143 阅读2分钟

CommonJS 规范是Node中使用的模块化规范。定义了模块 的导出和引入的方式,可以再多个文件之间共享 JavaScript 代码。

规范需要满足功能

  1. 每个js文件都是一个模块
  2. 每个模块想去引用别人的模块,需要采用require语法 import
  3. 每个模块想被别人使用需要采用 module.exports 进行导出 export

涉及的方法

fs.existsSync(id)

以同步的方法检测目录是否存在。

path.extname(id)

用于获取文件路径的扩展部分

vm.compileFunction(code[, params[, options]])

将字符串包装成函数,除了最后一个参数,真的和new Function没什么两样。

vm.compileFunction(content['module','exports','require','__dirname','__filename'])
支持沙箱 可以保证作用域不污染,可以指定函数中的this,手动指定上下文

Reflect.apply(target, thisArgument, argumentsList)

用于使用指定的参数调用函数。它的函数类似于调用函数的Function.prototype.apply()方法

 Reflect.apply(wrapperFunction,exports,[module,exports,require,__dirname,__filename])

实现思路

序号流程使用的方法
1创建模块,模块最终的导出的结果都在这里面主流程
2根据用户传递的id 来进行模块的加载,相对路径转换成绝对路径Module._resolveFilename
3同一个模块加载多次应该只允许一次,所以做缓存了Module._cache主流程
4根据文件名来加载模块 Module._load主流程
5根据后缀名来处理对应的模块Module._load
6实现Module._extensions方法,默认会查找同名的文件,会尝试添加后缀Module._extensions
7读取文件内容Module._extensions
8给文件内容添加一个函数 module._compileModule._extensions
9wrapSafe 给内容进行了包裹 (vm.compileFunction)Module._extensions
10执行函数Module._extensions
11最终返回的是 module.exports主流程

代码实现

// cocommonjs实现
const fs = require('fs');
const path = require('path');
const vm = require('vm')
function Module(id){
    this.id = id;
    this.exports = {}; // 模块最终的导出的结果都在这里面
}
Module._cache = {}; // 模块缓存
Module._extensions = {
    '.js'(module){
        const content = fs.readFileSync(module.id,'utf8');
        // 将字符串包装成函数, 也可以用new Function来直接实现
        const wrapperFunction = vm.compileFunction(content,['module','exports','require','__dirname','__filename'])
        let exports = module.exports; 
        let require = req;
        let __dirname = path.dirname(module.id); // 文件对应的目录
        let __filename = module.id; // 绝对路径
        Reflect.apply(wrapperFunction,exports,[module,exports,require,__dirname,__filename])
    },
    '.json'(module){
        // json如何处理
        const content = fs.readFileSync(module.id,'utf8');
        module.exports =  JSON.parse(content)
    }
}
Module._resolveFilename = function(id){
    // 默认会查找同名的文件,会尝试添加后缀 
    const exts =  Reflect.ownKeys(Module._extensions)
    const  url =path.resolve(__dirname,id)
    const isExists = fs.existsSync(url); // 不会抛错 fs.access 需要用tryCatch
    if(isExists) return url;
    // 先查找js在查找json
    for(let i = 0; i < exts.length;i++){
        let fileUrl = path.resolve(__dirname,id) + exts[i];
        if(fs.existsSync(fileUrl)){
            return fileUrl
        }
    }
    throw new Error('模块未找到')
}
Module.prototype.load = function(){
    const ext =  path.extname(this.id); // a.min".js"
    Module._extensions[ext](this); // 根据后缀名来处理对应的模块
}
function req(id){
    // 1.根据用户传递的id 来进行模块的加载,相对路径转换成绝对路径
    let absPath = Module._resolveFilename(id)
    // 2.创建模块
    let existsModule = Module._cache[absPath]; // 是否存在这个模块
    if(existsModule){
        return existsModule.exports; // 返回上一次导出的结果
    }
    const module = new Module(absPath); // 如果我多次require模块这个模块只会被读取一次
    Module._cache[absPath] = module;
    // 3.就是加载这个模块
    module.load() // 加载完模块后既可以拿到最终的模块导出结果
    return module.exports;
}
const result = req('./b.js')
console.log(result)
//b.js
module.exports = 'hello b'

console.log('ok')