关于前端:前端开发技术之require的原理分享

8次阅读

共计 8302 个字符,预计需要花费 21 分钟才能阅读完成。

咱们常说 node 并不是一门新的编程语言,他只是 javascript 的运行时,运行时你能够简略地了解为运行 javascript 的环境。在大多数状况下咱们会在浏览器中去运行 javascript,有了 node 的呈现,咱们能够在 node 中去运行 javascript,这意味着哪里装置了 node 或者浏览器,咱们就能够在哪里运行 javascript。前端培训

1.node 模块化的实现

node 中是自带模块化机制的,每个文件就是一个独自的模块,并且它遵循的是 CommonJS 标准,也就是应用 require 的形式导入模块,通过 module.export 的形式导出模块。

node 模块的运行机制也很简略,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就能够实现代码间的作用域隔离。

你可能会说,我在写代码的时候并没有包裹函数呀,是的的确如此,这一层函数是 node 主动帮咱们实现的,咱们能够来测试一下。

咱们新建一个 js 文件,在第一行打印一个并不存在的变量,比方咱们这里打印 window,在 node 中是没有 window 的。

console.log(window);
复制代码

通过 node 执行该文件,会发现报错信息如下。(请应用零碎默认 cmd 执行命令)。

(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)

复制代码

能够看到报错的顶层有一个自执行的函数,, 函数中蕴含 exports, require, module, __filename, __dirname 这些咱们罕用的全局变量。

我在之前的《前端模块化倒退历程》一文中介绍过。自执行函数也是前端模块化的实现计划之一,在晚期前端没有模块化零碎的时代,自执行函数能够很好的解决命名空间的问题,并且模块依赖的其余模块都能够通过参数传递进来。cmd 和 amd 标准也都是依赖自执行函数实现的。

在模块零碎中,每个文件就是一个模块,每个模块里面会主动套一个函数,并且定义了导出形式 module.exports 或者 exports,同时也定义了导入形式 require。

let moduleA = (function() {

module.exports = Promise;
return module.exports;

})();
复制代码

2.require 加载模块

require 依赖 node 中的 fs 模块来加载模块文件,fs.readFile 读取到的是一个字符串。

在 javascrpt 中咱们能够通过 eval 或者 new Function 的形式来将一个字符串转换成 js 代码来运行。

eval

const name = ‘yd’;
const str = ‘const a = 123; console.log(name)’;
eval(str); // yd;
复制代码

new Function
new Function 接管的是一个要执行的字符串,返回的是一个新的函数,调用这个新的函数字符串就会执行了。如果这个函数须要传递参数,能够在 new Function 的时候顺次传入参数,最初传入的是要执行的字符串。比方这里传入参数 b,要执行的字符串 str。

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();
复制代码

node 存在一个 vm 虚拟环境的概念,用来运行额定的 js 文件,他能够保障 javascript 执行的独立性,不会被内部所影响。

vm 内置模块

尽管咱们在内部定义了 hello,然而 str 是一个独立的模块,并不在村 hello 变量,所以会间接报错。

// 引入 vm 模块,不须要装置,node 自建模块
const vm = require(‘vm’);
const hello = ‘yd’;
const str = ‘console.log(hello)’;
wm.runInThisContext(str); // 报错
复制代码

所以 node 执行 javascript 模块时能够采纳 vm 来实现。就能够保障模块的独立性了。

3.require 代码实现

介绍 require 代码实现之前先来回顾两个 node 模块的用法,因为上面会用失去。

path 模块

用于解决文件门路。

basename: 根底门路, 有文件门路就不是根底门路,根底路劲是 1.js

extname: 获取扩展名

dirname: 父级路劲

join: 拼接门路

resolve: 以后文件夹的绝对路径,留神应用的时候不要在结尾增加 /

__dirname: 以后文件所在文件夹的门路

__filename: 以后文件的绝对路径

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’));
复制代码

fs 模块

用于操作文件或者文件夹,比方文件的读写,新增,删除等。罕用办法有 readFile 和 readFileSync,别离是异步读取文件和同步读取文件。

const fs = require(‘fs’);
const buffer = fs.readFileSync(‘./name.txt’, ‘utf8’); // 如果不传入编码,进去的是二进制
console.log(buffer);
复制代码

fs.access: 判断是否存在,node10 提供的,exists 办法曾经被废除, 起因是不合乎 node 标准,所以咱们采纳 access 来判断文件是否存在。

try {

fs.accessSync('./name.txt');

} catch(e) {

// 文件不存在

}
复制代码

4. 手动实现 require 模块加载器

首先导入依赖的模块 path,fs, vm, 并且创立一个 Require 函数,这个函数接管一个 modulePath 参数,示意要导入的文件门路。

// 导入依赖
const path = require(‘path’); // 门路操作
const fs = require(‘fs’); // 文件读取
const vm = require(‘vm’); // 文件执行

// 定义导入类,参数为模块门路
function Require(modulePath) {

...

}
复制代码

在 Require 中获取到模块的绝对路径,方便使用 fs 加载模块,这里读取模块内容咱们应用 new Module 来形象,应用 tryModuleLoad 来加载模块内容,Module 和 tryModuleLoad 咱们稍后实现,Require 的返回值应该是模块的内容,也就是 module.exports。

// 定义导入类,参数为模块门路
function Require(modulePath) {

// 获取以后要加载的绝对路径
let absPathname = path.resolve(__dirname, modulePath);
// 创立模块,新建 Module 实例
const module = new Module(absPathname);
// 加载以后模块
tryModuleLoad(module);
// 返回 exports 对象
return module.exports;

}
复制代码

Module 的实现很简略,就是给模块创立一个 exports 对象,tryModuleLoad 执行的时候将内容退出到 exports 中,id 就是模块的绝对路径。

// 定义模块, 增加文件 id 标识和 exports 属性
function Module(id) {

this.id = id;
// 读取到的文件内容会放在 exports 中
this.exports = {};

}
复制代码

之前咱们说过 node 模块是运行在一个函数中,这里咱们给 Module 挂载动态属性 wrapper,外面定义一下这个函数的字符串,wrapper 是一个数组,数组的第一个元素就是函数的参数局部,其中有 exports,module. Require,__dirname, __filename, 都是咱们模块中罕用的全局变量。留神这里传入的 Require 参数是咱们本人定义的 Require。

第二个参数就是函数的完结局部。两局部都是字符串,应用的时候咱们将他们包裹在模块的字符串内部就能够了。

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 属性上
}

}
复制代码

tryModuleLoad 函数接管的是模块对象,通过 path.extname 来获取模块的后缀名,而后应用 Module._extensions 来加载模块。

// 定义模块加载办法
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 和 json
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);

}
复制代码

5. 给模块增加缓存

增加缓存也比较简单,就是文件加载的时候将文件放入缓存在,再去加载模块时先看缓存中是否存在,如果存在间接应用,如果不存在再去从新嘉爱,加载之后再放入缓存。

// 定义导入类,参数为模块门路
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;

}
复制代码

6. 主动补全门路

主动给模块增加后缀名,实现省略后缀名加载模块,其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在。

// 定义导入类,参数为模块门路
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;

}
复制代码

7. 剖析实现步骤

1. 导入相干模块,创立一个 Require 办法。

2. 抽离通过 Module._load 办法,用于加载模块。

3.Module.resolveFilename 依据相对路径,转换成绝对路径。

4. 缓存模块 Module._cache,同一个模块不要反复加载,晋升性能。

5. 创立模块 id: 保留的内容是 exports = {}相当于 this。

6. 利用 tryModuleLoad(module, filename) 尝试加载模块。

7.Module._extensions 应用读取文件。

8.Module.wrap: 把读取到的 js 包裹一个函数。

9. 将拿到的字符串应用 runInThisContext 运行字符串。

10. 让字符串执行并将 this 改编成 exports。

作者:隐冬

正文完
 0