手寫CommonJS 中的 require函數
前言
什么是 CommonJS ?
node.js 的应用采用的commonjs模块规范。
每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS规范规定:每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。require方法用于加载模块。
CommonJS模块的特点:
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
如何使用?
假设我们现在有个a.js文件,我们要在main.js 中使用a.js的一些方法和变量,运行环境是nodejs。这样我们就可以使用CommonJS规范,让a文件导出方法/变量。然后使用require函数引入变量/函数。
示例:
// a.js module.exports = '这是a.js的变量'; // 导出一个变量/方法/对象都可以
// main.js
// 这里如果导入a.js,那么他会自动按照预定顺序帮你添加后缀
let str = require('./a');
// 输出:'这是a.js的变量'
console.log(str);手写一个require函数
前言
我们现在就开始手写一个 精简版的 require函数,这个require函数支持以下功能:
导入一个符合CommonJS规范的JS文件。
支持自动添加文件后缀(暂时支持JS和JSON文件)
现在就开始吧!
1. 定义一个req方法
我们先自定义一个req方法,和全局的require函数隔离开。
这个req方法,接受一个名为ID的参数,也就是要加载的文件路径。
// main.js
function req(id){}
let a = req('./a')
console.log(a)2. 新建一个Module 类
新建一个module类,这个module将会处理文件加载的全过程。
function Module(id) {
this.id = id; // 当前模块的文件路径
this.exports = {} // 当前模块导出的结果,默认为空
}3. 获取文件绝对路径
刚才我们介绍到,require 函数支持传入一个路径。这个路径可以是相对路径,也可以是绝对路径,也可以不写文件后缀名。
我们在Module类上添加一个叫做“_resolveFilename” 的方法,用于解析用户传进去的文件路径,获取一个绝对路径。
// 将一个相对路径 转化成绝对路径
Module._resolveFilename = function (id) {}继续添加一个 “extennsions” 的属性,这个属性是一个对象。key是文件扩展名,value就是扩展名对应的不同文件的处理方法。
我们通过debugger nodejs require源码看到,原生的require函数支持四种类型文件:
js文件
json文件
node文件
mjs文件
由于篇幅,这里我们就只支持两个扩展名:.js 和.json。
我们分别在extensions对象上,添加两个属性,两个属性的值分别都是一个函数。方便不同文件类型分类处理。
// main.js
Module.extensions['.js'] = function (module) {}
Module.extensions['.json'] = function (module) {}接着,我们导入nodejs原生的“path”模块和“fs”模块,方便我们获取文件绝对路径和文件操作。
我们处理一下 Module._resolveFilename 这个方法,让他可以正常工作。
Module._resolveFilename = function (id) {
// 将相对路径转化成绝对路径
let absPath = path.resolve(id);
// 先判断文件是否存在如果存在了就不要增加了
if(fs.existsSync(absPath)){
return absPath;
}
// 去尝试添加文件后缀 .js .json
let extenisons = Object.keys(Module.extensions);
for (let i = 0; i < extenisons.length; i++) {
let ext = extenisons[i];
// 判断路径是否存在
let currentPath = absPath + ext;
// 获取拼接后的路径
let exits = fs.existsSync(currentPath);
// 判断是否存在
if(exits){
return currentPath
}
}
throw new Error('文件不存在')
}在这里,我们支持接受一个名id的参数,这个参数将是用户传来的路径。
首先我们先使用 path.resolve()获取到文件绝对路径。接着用 fs.existsSync 判断文件是否存在。如果没有存在,我们就尝试添加文件后缀。
我们会去遍历现在支持的文件扩展对象,尝试拼接路径。如果拼接后文件存在,返回文件路径。不存在抛出异常。
这样我们在req方法内,就可以获取到完整的文件路径:
function req(id){
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
}4. 加载模块 —— JS的实现
这里就是我们的重头戏,加载common.js模块。
首先 new 一个Module实例。传入一个文件路径,然后返回一个新的module实例。
接着定义一个 tryModuleLoad 函数,传入我们新建立的module实例。
function tryModuleLoad(module) {
// 尝试加载模块
let ext = path.extname(module.id);
Module.extensions[ext](module)
}function req(id){
// 通过相对路径获取绝对路径
let filename = Module._resolveFilename(id);
let module = new Module(filename);
// new 一个新模块
tryModuleLoad(module);
}tryModuleLoad 函数 获取到module后,会使用 path.extname 函数获取文件扩展名,接着按照不同扩展名交给不同的函数分别处理。
接下来,我们处理js文件加载.
第一步,传入一个module对象实例。
使用module对象中的id属性,获取文件绝对路径。拿到文件绝对路径后,使用fs模块读取文件内容。读取编码是utf8。
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
}第二步,伪造一个自执行函数。
这里先新建一个wrapper 数组。数组的第0项是自执行函数开头,最后一项是结尾。
let wrapper = [
'(function (exports, require, module, __dirname, __filename) {\r\n',
'\r\n})'
];这个自执行函数需要传入5个参数:exports对象,require函数,module对象,dirname路径,fileame文件名。
我们将获取到的要加载文件的内容,和自执行函数模版拼接,组装成一个完整的可执行js文本:
Module.extensions['.js'] = function (module) {
// 1) 读取
let script = fs.readFileSync(module.id, 'utf8');
// 2) 内容拼接
let content = wrapper[0] + script + wrapper[1];
}第三步:创建沙箱执行环境
这里我们就要用到nodejs中的 “vm” 模块了。这个模块可以创建一个nodejs的虚拟机,提供一个独立的沙箱运行环境。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章

