学习笔记—Node中require的实现
日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。
require
在上一篇文章中,我们了解到了如何去通过 调试查看Node源码。
// 使用 require 引入文件
// a.js
var a = 100;
module.exports = a;
// b.js
let a = require('./a')
console.log(a)
通过调试查看 require
方法源码,其实现思路主要为以下几点。
require
方法的是Module
模块的原型方法,也就是Module.prototype.require
。- 通过
Module._resolveFilename
方法,将传入的路径转换为绝对路径。并添加文件的后缀名。(.js、.json 等) new Module
拿到转换完毕的绝对路径,并创造一个模块并导出。(其中包含一个属性id [ 当前文件路径 ],还有一个 exports)Module.load
对模块进行加载。- 根据文件后缀
Module._extensions['.js']
去做策略加载。 fs.readFileSync
同步读取文件。- 增加了一个函数的外壳 ( wrapper包装 ) 让这个函数执行,并且让
Module.exports
作为当前上下文的this
。 - 最终用户会拿到
Module.exports
的封装后的返回结果。
所以,最终会返回一个 Module.exports
对象。通过以上思路,我们就可以实现一套 require
方法。
实现require方法
根据上述规则,我们可以模拟实现一套 require
方法。
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function Module(id) {
this.id = id;
this.exports = {};
}
Module.wrapper = [
`(function(exports,require,module,__filename,__dirname){`,
`})`
];
Module._extensions = {
'.js'(module) {
let content = fs.readFileSync(module.id, 'utf8');
content = Module.wrapper[0] + content + Module.wrapper[1];
let fn = vm.runInThisContext(content);
let exports = module.exports;
let dirname = path.dirname(module.id);
fn.call(exports, exports, _require, module, module.id, dirname);
},
'.json'(module) {
let content = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(content);
}
}
Module._resolveFilename = function (filename) {
let filePath = path.resolve(__dirname, filename);
let isExists = fs.existsSync(filePath);
if (isExists) {
return absPath;
} else {
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let newPath = filePath + keys;
if (fs.existsSync(newPath)) return newPath;
}
throw new Error('module not exists')
}
}
Module.prototype.load = function () {
let extName = path.extname(this.id);
Module._extensions[extName](this);
}
Module._cache = {};
function _require(filename) {
filename = Module._resolveFilename(filename);
let cacheModule = Module._cache[filename];
if (cacheModule) {
return cacheModule.exports;
}
let module = new Module(filename);
Module._cache[filename] = module
module.load();
return module.exports;
}
现在我们来进行一步一步的解析。
-
首先,我们需要先引入需要用到的内置模块(
fs
、path
和vm
)。自定义一个 函数方法
_require
,这就是我们最终需要实现的方法,第一个参数接受传入的路径。const fs = require('fs'); const path = require('path'); const vm = require('vm'); function _require(filename) { // ... }
-
随后我们还需要一个
Module._resolveFilename
方法将传入的路径转换成绝对路径,并添加后缀。因为使用
Module
方法,我们我们也需要声明一个名为Module
的构造函数。随后将路径传入
Module._resolveFilename
方法中。function Module() {} Module._resolveFilename = function (id) { let filePath = path.resolve(__dirname, id); console.log(filePath); // d:\xxx\xxx\xxx\a } function _require(filename) { filename = Module._resolveFilename(filename); // 绝对路径 }
-
我们发现目前打印的结果是没有后缀的(不确定用户是否填写后缀),所以我们需要使用
fs.existsSync
判断当前路径是否存在。Module._resolveFilename = function (id) { // ... let isExists = fs.existsSync(filePath); if (isExists) return filePath; }
如果存在,则直接返回结果。如果不存在,我们就需要给当前路径尝试添加后缀。
-
这里我们就需要添加后缀,我们需要先定义一个
Module._extensions
方法来对后缀进行分类。Module._extensions = { '.js'() {}, '.json'() {} }
然后我们将定义的后缀方法的
keys
添加到路径上,并再次进行路径判断。路径存在则直接返回结果,如果路径不存在,这次就需要 返回一个错误。
Module._resolveFilename = function (id) { if(isExists){ // ... } else { let keys = Object.keys(Module._extensions); for (let i = 0; i < keys.length; i++) { let newPath = filePath + keys; if (fs.existsSync(newPath)) return newPath; } throw new Error('module not exists') } }
这样就可以保证我们 传入的路径,无论是加后缀或者不加后缀,都会返回一个 当前路径的绝对路径,且 一定会找到当前文件。
-
我们就已经创建好了一个 绝对引用路径,方便我们后续进行读取。
现在我们就需要根据这个路径,创建一个可以导出的模块。
这个模块就属于
Module
构造函数,根据我们一开始查看源码时总结的定义,我们知道 模块全部都是通过Module.exports
方法进行导出的。function Module(id) { this.id = id; // 绝对路径 this.exports = {}; // 默认导出的是空对象 } function _require(filename) { filename = Module._resolveFilename(filename); // 绝对路径 let module = new Module(filename); return module.exports; }
这样我们路径和导出的架子就有了,现在我们需要对中间部分进行处理。
-
其实所谓的 中间部分,也就是让用户对
Module.exports
赋值(目前导出的是空对象)。根据源码的定义,我们需要定义一个
module.load
来对模块进行加载。Module.prototype.load = function () { let extName = path.extname(this.id); // 获取后缀名 Module._extensions[extName](this); } function _require(filename) { // ... module.load(); }
这种定义的好处就是,我们可以根据传入的后缀名,调用不同的处理策略。实现文件的 策略加载。
这样我们的
module
就会被传到上面的Module._extensions
方法中Module._extensions = { '.js'(module) {}, '.json'(module) {} }
下一步我们就需要完善一下
Module._extensions
方法。 -
我们先来完善一下
json
方法,因为这个是最好实现的。我们先随便定义一个
.json
文件来进行测试。// a.json { "name" : "MXShang", "age" : 26 }
然后我们来完善一下
Module._extensions[json]
方法Module._extensions = { '.js'() {}, '.json'(module) { let content = fs.readFileSync(module.id, 'utf8') module.exports = JSON.parse(content); } }
获取绝对路径,通过
fs.readFileSync
同步读取内容,并输出。很好理解也很简单,接下来我们看一下
Module._extensions[js]
。 -
在实现
Module._extensions[js]
方法前,我们先需要完成一个函数的外壳 ( wrapper包装 ) ,也就是我们之前文章中经常提到的,包含五个参数的函数。Module.wrapper = [ `(function(exports,require,module,__filename,__dirname){`, `})` ];
然后我们来实现
Module._extensions[js]
方法。思路与实现
.json
相似,先将绝对路径的内容读出来,并将内容放到 wrapper 中。。Module._extensions = { '.js'(module) { let content = fs.readFileSync(module.id, 'utf8'); content = Module.wrapper[0] + content + Module.wrapper[1]; } }
这样我们就可以得到一个 被wrapper包裹的代码字符串。
-
现在我们来将字符串变成可以执行的函数。使用
vm.runInThisContext
将字符串变成函数。Module._extensions = { '.js'(module) { // ... let fn = vm.runInThisContext(content); // 获取最终执行的函数 } }
现在我们需要明确一下
fn
的执行位置,也就是其 this的指向。不用多说,this一定是指向
module.exports
的,所以我们需要通过fn.call
方法来将函数的this指向当前构造函数。然后我们再根据最终的函数,依次获取一下需要传递的五个参数。
Module._extensions = { '.js'(module) { // ... let exports = module.exports; // 当前this也就是exports参数。this = exports = module.exports let dirname = path.dirname(module.id); // 当前文件执行位置的绝对略经 fn.call(exports, exports, _require, module, module.id, dirname); } }
到了这一步我们就可以发现,
require
方法实际上就是通过Module
作为一个 中间层 来实现的。至此,我们的
require
方法的整体思路就实现了。但是我们还有一个小问题,就是如果我们多次引入文件,是没有缓存的。所以我们需要 对结果进行缓存 。
-
定义一个
Module._cache
来对结果进行缓存。// 定义一个 Module._cache Module._cache = {}; // 在模块加载前,先将定义好的module模块结果进行缓存 function _require(filename) { // ... Module._cache[filename] = module; // 根据文件名进行缓存 module.load(); return module.exports; }
如果当前模块已经被缓存过 (加载过) ,直接将缓存好的模块导出就可以了。
function _require(filename) { // ... let cacheModule = Module._cache[filename]; if (cacheModule) { return cacheModule.exports; } let module = new Module(filename); Module._cache[filename] = module; // 根据文件名进行缓存 module.load(); return module.exports; }
这样我们就实现了一套
require
方法。
通过阅读源码,并通过源码手写方法,可以使我们更好的使用方法,也可以提升我们的技术。
本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点、博客园 和 掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!