前言
模块化是一个神奇的东西,好多语言里面都存在模块化,模块化的原理是什么,想必很多小伙伴都很感兴趣,今天我们就手写node的require.js来让大家更深入的理解模块化
完整代码可以参考 github代码库:github.com/Y-wson/Dail…
简单的例子
首先建立一个文件a
let a = 12;
a = a + 1;
module.exports = a;
在建立一个文件b
const a = require("./v");
console.log(a);
无疑答案是13,从上面步骤我们可以看出,先执行整个文件a,然后把a的执行结果,赋值给module.exports,文件b中在取出module.exports的值,赋值给变量a,最后打印变量a
接下来我们先来了解一些基本概念
模块分类
- 内置模块:就是Node.js原生提供的功能,比如
fs,http等等,这些都是Node.js的核心模块,这些模块在Node.js进程起来时就加载了。- 文件模块:第三方的模块以及自定义模块
执行流程
1.路径分析: 依据标识符确定模块位置
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去
node_modules下面找。 - 还找不到就报错了。
加载文件夹
前面提到找不到文件就找文件夹,但是不可能将整个文件夹都加载进来,加载文件夹的时候也是有一个加载顺序的:
- 先看看这个文件夹下面有没有
package.json,如果有就找里面的main字段,main字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他package.json里面的main字段吧,比如jquery的main字段就是这样:"main": "dist/jquery.js"。- 如果没有
package.json或者package.json里面没有main就找index文件。- 如果这两步都找不到就报错了。
2.文件定位: 确定目标模块中具体的文件及文件类型
支持的文件类型
require主要支持三种文件类型:
- .js:
.js文件是我们最常用的文件类型,加载的时候会先运行整个JS文件,然后将前面说的module.exports作为require的返回值。- .json:
.json文件是一个普通的文本文件,直接用JSON.parse将其转化为对象返回就行。- .node:
.node文件是C++编译后的二进制文件,纯前端一般很少接触这个类型。
3.编译执行: 采用对应的方式完成文件的编译执行
我们会用vm去执行文件
手写源码
模块加载的所有功能都在Module类里面,为了区别原生的Module类,我们给我们的模块取名为MyModule
首先建立一个MyModule.js文件,然后初始化我们的文件
function MyModule(id = "") {
this.id = id;
this.exports = {};
}
MyModule._extension = {
".js": () => {},
".json": () => {},
};
MyModule.require = (request) => {
console.log(request);
};
接下来我们先进行第一步路径分析,此处我们只是小白级教程,所以只讲解加载自定义模块,那么我们自定义一个方法resolveFileName
MyModule.resolveFileName = (request) => {
let filename = path.resolve(request);
const extname = path.extname(filename);
if (extname) {
return filename;
} else {
const suffixArr = Object.keys(MyModule._extension);
for (let suffix of suffixArr) {
filename = `${filename}${suffix}`;
if (fs.existsSync(filename)) {
return filename;
}
}
}
};
然后是看看缓存里面有没有这个模块,如果有的话,那么我们直接进行加载,如果没有的话,我们就开始load模块
MyModule._cache = [];
MyModule.require = (request) => {
const filename = MyModule.resolveFileName(request);
// 判断缓存里面是否有模块
const cacheModule = MyModule._cache[filename];
if (cacheModule) return cacheModule.exports;
// 初始化一个新的模块
const module = new MyModule(filename);
// load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整
MyModule._cache[filename] = module;
module.load(module);
// 返回模块的exports
return module.exports;
};
load函数其实就是我们的第二步,确定文件类型,执行对应的加载文件的方法
MyModule.load = (module) => {
const extname = path.extname(module.id);
// 根据后缀执行文件内容
MyModule._extension[extname](module);
};
接下来就是执行文件了,首先我们定义js文件如何执行
注意点: 我们知道在模块内有几个类似于全局变量的变量,比如__filename, __dirname,require等,这些都是如何来的呀,其实就是通过函数传过去的,并不是真正的全局变量
// 定义匿名函数
MyModule._wrapper = [
`(function(exports,require,module,__filename,__dirname){`,
`})`,
];
MyModule._extension = {
".js": (module) => {
// 获取文件内容
const content = fs.readFileSync(module.id, "utf-8");
// 准备好全局变量传给模块内部
const filename = module.id;
const dirname = path.dirname(filename);
// vm是用来执行字符串的库
const fun = vm.runInThisContext(
MyModule._wrapper[0] + content + MyModule._wrapper[1]
);
fun.call(
module.exports,
module.exports,
MyModule.require,
module,
filename,
dirname
);
},
".json": (module) => {},
};
接下来定义一下json文件如何执行,json文件执行要比js简单的多
".json": (module) => {
const content = fs.readFileSync(module.id, "utf-8");
module.exports = JSON.parse(content);
},
好啦,我们自己的MyModule就大功告成了
小伙伴们可以自己引用测试一下
总结
- 1.手写require源码的三个步骤1.路径分析2.文件定位3.编译执行
- 2.编译执行其实是用到了vm这个库,来执行字符串的,相对于eval,function等更好用
- 3.每个模块里面的
exports, require, module, __filename, __dirname五个参数都不是全局变量,而是模块加载的时候注入的。