发布于 2024 年 10 月 21 日,星期一
文章详细介绍了require加载器的工作机制,包括模块的查找、加载和缓存过程,以及如何通过模块路径解析和依赖管理来实现模块的动态加载。此外,还讨论了require加载器在不同环境下的实现差异,如Node.js和浏览器端的区别,以及如何通过配置和插件来优化模块加载性能。通过深入理解require加载器的本质,开发者可以更高效地管理和组织前端代码,提升应用的性能和可维护性。
Node.js 中 require 加载器的原理?
node模块化的实现?
node 是自带模块化机制的,每个文件是一个单独的模块,并且遵循的是 commonjs 规范,也就是使用
require 方式导入模块,通过 module.exports 导出模块
node 模块的运行机制也很简单,就是再每一个模块外层包裹一层函数,有了函数的包裹就可以实现代码
间的作用域隔离。
eg:
console.log(window);(function (exports, require, module, __filename, __dirname) { console.log(window);ReferenceError: window is not defined at Object.<anonymous> (/Users/choice/Desktop/node/main.js:1:75) at Module._compile (internal/modules/cjs/loader.js:689:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) at Module.load (internal/modules/cjs/loader.js:599:32) at tryModuleLoad (internal/modules/cjs/loader.js:538:12) at Function.Module._load (internal/modules/cjs/loader.js:530:3) at Function.Module.runMain (internal/modules/cjs/loader.js:742:12) at startup (internal/bootstrap/node.js:279:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:752:3))
let moduleA = (funciton(){ module.exports = Promise; return module.exports;})()
require 加载模块?
eval、new Function
的方式将一个字符串转换为 js 代码来执行。// evalconst name = 'ylf';const str = 'const a = 123; console.log(name)';eval(str); // ylf/**
* new Function
* 定义:new Funcition 接受一个要执行的字符串,返回一个新函数,调用这个新函数字符串就会执行。如果
* 这个函数需要传递参数,可在 new Function 的时候一次传入参数,最后传入要执行的字符串。
*/const b = 3;const str = 'let a = 1; return a + b;';const fun = new Function('b', str);console.log(fun(b, str)); // 4
可以看到 eval 和 Function 实例化都可以用来执行 javascript 字符串,似乎他们都可以来实现 require 模块加载。不过在 node 中并没有选用他们来实现模块化,原因也很简单因为他们都有一个致命的问题,就是都容易被不属于他们的变量所影响。
如下 str 字符串中并没有定义 a,但是确可以使用上面定义的 a 变量,这显然是不对的,在模块化机制中,str 字符串应该具有自身独立的运行空间,自身不存在的变量是不可以直接使用的。
const a = 1;const str = 'console.log(a)';eval(str);const func = new Function(str);func();
vm 内置模块
// 引入vm模块, 不需要安装,node 自建模块const vm = require('vm');const hello = 'yd';const str = 'console.log(hello)';wm.runInThisContext(str); // 报错
require 代码实现
1.js
/
const path = require('path', 's');console.log(path.basename('1.js'));console.log(path.extname('2.txt'));console.log(path.dirname('2.txt'));console.log(path.join('a/b/c', 'd/e/f')); // a/b/c/d/e/console.log(path.resolve('2.txt'));
// 基本const fs = require('fs');const buffer = fs.readFileSync('./name.txt', 'utf8'); // 如果不传入编码,出来的是二进制console.log(buffer);// fs.accesstry { fs.accessSync('./name.txt');} catch (e) { // 文件不存在}
实现 require 模块加载器
// 导入依赖const path = require('path'); // 路径操作const fs = require('fs'); // 文件读取const vm = require('vm'); // 文件执行// 定义导入类,参数为模块路径function Require(modulePath) { ...}
// 定义导入类,参数为模块路径function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 创建模块,新建Module实例 const module = new Module(absPathname); // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports;}
// 定义模块, 添加文件id标识和exports属性function Module(id) { this.id = id; // 读取到的文件内容会放在exports中 this.exports = {};}
Module.wrapper = ['(function(exports, module, Require, __dirname, __filename) {', '})'];
_extensions 用于针对不同的模块扩展名使用不同的加载方式,比如 JSON 和 javascript 加载方式肯定是不同的。
JSON 使用 JSON.parse 来运行。
javascript 使用 vm.runInThisContext 来运行,可以看到 fs.readFileSync 传入的是 module.id 也就是我们 Module
定义时候 id 存储的是模块的绝对路径,读取到的 content 是一个字符串,我们使用 Module.wrapper 来包裹一下就相当
于在这个模块外部又包裹了一个函数,也就实现了私有作用域。
使用 call 来执行 fn 函数,第一个参数改变运行的 this 我们传入 module.exports,后面的参数就是函数外面包裹参数
exports, module, Require, **dirname, **filename
Module._extensions = { '.js'(module) { const content = fs.readFileSync(module.id, 'utf8'); const fnStr = Module.wrapper[0] + content + Module.wrapper[1]; const fn = vm.runInThisContext(fnStr); fn.call(module.exports, module.exports, module, Require, _filename, _dirname); }, '.json'(module) { const json = fs.readFileSync(module.id, 'utf8'); module.exports = JSON.parse(json); // 把文件的结果放在exports属性上 },};
// 定义模块加载方法function tryModuleLoad(module) { // 获取扩展名 const extension = path.extname(module.id); // 通过后缀加载当前模块 Module._extensions[extension](module);}
Require 加载机制我们基本就写完了,我们来重新看一下。Require 加载模块的时候传入模块名称,在 Require
方法中使用 path.resolve(__dirname, modulePath)获取到文件的绝对路径。然后通过 new Module 实例化的方式
创建 module 对象,将模块的绝对路径存储在 module 的 id 属性中,在 module 中创建 exports 属性为一个 json 对象。
使用 tryModuleLoad 方法去加载模块,tryModuleLoad 中使用 path.extname 获取到文件的扩展名,然后根据扩展
名来执行对应的模块加载机制。
最终将加载到的模块挂载 module.exports 中。tryModuleLoad 执行完毕之后 module.exports 已经存在了,
直接返回就可以了。
// 导入依赖const path = require('path'); // 路径操作const fs = require('fs'); // 文件读取const vm = require('vm'); // 文件执行// 定义导入类,参数为模块路径function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 创建模块,新建Module实例 const module = new Module(absPathname); // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports;}// 定义模块, 添加文件id标识和exports属性function Module(id) { this.id = id; // 读取到的文件内容会放在exports中 this.exports = {};}// 定义包裹模块内容的函数Module.wrapper = ['(function(exports, module, Require, __dirname, __filename) {', '})'];// 定义扩展名,不同的扩展名,加载方式不同,实现js和jsonModule._extensions = { '.js'(module) { const content = fs.readFileSync(module.id, 'utf8'); const fnStr = Module.wrapper[0] + content + Module.wrapper[1]; const fn = vm.runInThisContext(fnStr); fn.call(module.exports, module.exports, module, Require, _filename, _dirname); }, '.json'(module) { const json = fs.readFileSync(module.id, 'utf8'); module.exports = JSON.parse(json); // 把文件的结果放在exports属性上 },};// 定义模块加载方法function tryModuleLoad(module) { // 获取扩展名 const extension = path.extname(module.id); // 通过后缀加载当前模块 Module._extensions[extension](module);}
给模块添加缓存?
// 定义导入类,参数为模块路径function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 从缓存中读取,如果存在,直接返回结果 if (Module._cache[absPathname]) { return Module._cache[absPathname].exports; } // 尝试加载当前模块 tryModuleLoad(module); // 创建模块,新建Module实例 const module = new Module(absPathname); // 添加缓存 Module._cache[absPathname] = module; // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports;}
自动补全路径?
// 定义导入类,参数为模块路径function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 获取所有后缀名 const extNames = Object.keys(Module._extensions); let index = 0; // 存储原始文件路径 const oldPath = absPathname; function findExt(absPathname) { if (index === extNames.length) { return throw new Error('文件不存在'); } try { fs.accessSync(absPathname); return absPathname; } catch (e) { const ext = extNames[index++]; findExt(oldPath + ext); } } // 递归追加后缀名,判断文件是否存在 absPathname = findExt(absPathname); // 从缓存中读取,如果存在,直接返回结果 if (Module._cache[absPathname]) { return Module._cache[absPathname].exports; } // 尝试加载当前模块 tryModuleLoad(module); // 创建模块,新建Module实例 const module = new Module(absPathname); // 添加缓存 Module._cache[absPathname] = module; // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports;}
分析实现步骤
导入相关模块,创建一个 Require 方法。
抽离通过 Module._load 方法,用于加载模块。
Module.resolveFilename 根据相对路径,转换成绝对路径。
缓存模块 Module._cache,同一个模块不要重复加载,提升性能。
创建模块 id: 保存的内容是 exports = {}相当于 this。
利用 tryModuleLoad(module, filename) 尝试加载模块。
Module._extensions 使用读取文件。
Module.wrap: 把读取到的 js 包裹一个函数。
将拿到的字符串使用 runInThisContext 运行字符串。
让字符串执行并将 this 改编成 exports。