CommonJS 规范是Node中使用的模块化规范。定义了模块 的导出和引入的方式,可以再多个文件之间共享 JavaScript 代码。
规范需要满足功能
- 每个js文件都是一个模块
- 每个模块想去引用别人的模块,需要采用require语法 import
- 每个模块想被别人使用需要采用 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._compile | Module._extensions |
| 9 | wrapSafe 给内容进行了包裹 (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')