require加載器實現(xiàn)原理的深入理解
前言
我們常說node并不是一門新的編程語言,他只是javascript的運行時,運行時你可以簡單地理解為運行javascript的環(huán)境。在大多數(shù)情況下我們會在瀏覽器中去運行javascript,有了node的出現(xiàn),我們可以在node中去運行javascript,這意味著哪里安裝了node或者瀏覽器,我們就可以在哪里運行javascript。
1.node模塊化的實現(xiàn)
node中是自帶模塊化機制的,每個文件就是一個單獨的模塊,并且它遵循的是CommonJS規(guī)范,也就是使用require的方式導入模塊,通過module.export的方式導出模塊。
node模塊的運行機制也很簡單,其實就是在每一個模塊外層包裹了一層函數(shù),有了函數(shù)的包裹就可以實現(xiàn)代碼間的作用域隔離。
你可能會說,我在寫代碼的時候并沒有包裹函數(shù)呀,是的的確如此,這一層函數(shù)是node自動幫我們實現(xiàn)的,我們可以來測試一下。
我們新建一個js文件,在第一行打印一個并不存在的變量,比如我們這里打印window,在node中是沒有window的。
console.log(window);
通過node執(zhí)行該文件,會發(fā)現(xiàn)報錯信息如下。(請使用系統(tǒng)默認cmd執(zhí)行命令)。
(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)
可以看到報錯的頂層有一個自執(zhí)行的函數(shù),, 函數(shù)中包含exports, require, module, __filename, __dirname這些我們常用的全局變量。
我在之前的《前端模塊化發(fā)展歷程》一文中介紹過。自執(zhí)行函數(shù)也是前端模塊化的實現(xiàn)方案之一,在早期前端沒有模塊化系統(tǒng)的時代,自執(zhí)行函數(shù)可以很好的解決命名空間的問題,并且模塊依賴的其他模塊都可以通過參數(shù)傳遞進來。cmd和amd規(guī)范也都是依賴自執(zhí)行函數(shù)實現(xiàn)的。
在模塊系統(tǒng)中,每個文件就是一個模塊,每個模塊外面會自動套一個函數(shù),并且定義了導出方式 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接收的是一個要執(zhí)行的字符串,返回的是一個新的函數(shù),調(diào)用這個新的函數(shù)字符串就會執(zhí)行了。如果這個函數(shù)需要傳遞參數(shù),可以在new Function的時候依次傳入?yún)?shù),最后傳入的是要執(zhí)行的字符串。比如這里傳入?yún)?shù)b,要執(zhí)行的字符串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實例化都可以用來執(zhí)行javascript字符串,似乎他們都可以來實現(xiàn)require模塊加載。不過在node中并沒有選用他們來實現(xiàn)模塊化,原因也很簡單因為他們都有一個致命的問題,就是都容易被不屬于他們的變量所影響。
如下str字符串中并沒有定義a,但是確可以使用上面定義的a變量,這顯然是不對的,在模塊化機制中,str字符串應該具有自身獨立的運行空間,自身不存在的變量是不可以直接使用的。
const a = 1; const str = 'console.log(a)'; eval(str); const func = new Function(str); func();
node存在一個vm虛擬環(huán)境的概念,用來運行額外的js文件,他可以保證javascript執(zhí)行的獨立性,不會被外部所影響。
vm 內(nèi)置模塊
雖然我們在外部定義了hello,但是str是一個獨立的模塊,并不在村hello變量,所以會直接報錯。
// 引入vm模塊, 不需要安裝,node 自建模塊 const vm = require('vm'); const hello = 'yd'; const str = 'console.log(hello)'; wm.runInThisContext(str); // 報錯
所以node執(zhí)行javascript模塊時可以采用vm來實現(xiàn)。就可以保證模塊的獨立性了。
3.require代碼實現(xiàn)
介紹require代碼實現(xiàn)之前先來回顧兩個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方法已經(jīng)被廢棄, 原因是不符合node規(guī)范,所以我們采用access來判斷文件是否存在。
try { fs.accessSync('./name.txt'); } catch(e) { // 文件不存在 }
4.手動實現(xiàn)require模塊加載器
首先導入依賴的模塊path,fs, vm, 并且創(chuàng)建一個Require函數(shù),這個函數(shù)接收一個modulePath參數(shù),表示要導入的文件路徑。
// 導入依賴 const path = require('path'); // 路徑操作 const fs = require('fs'); // 文件讀取 const vm = require('vm'); // 文件執(zhí)行 // 定義導入類,參數(shù)為模塊路徑 function Require(modulePath) { ... }
在Require中獲取到模塊的絕對路徑,方便使用fs加載模塊,這里讀取模塊內(nèi)容我們使用new Module來抽象,使用tryModuleLoad來加載模塊內(nèi)容,Module和tryModuleLoad我們稍后實現(xiàn),Require的返回值應該是模塊的內(nèi)容,也就是module.exports。
// 定義導入類,參數(shù)為模塊路徑 function Require(modulePath) { // 獲取當前要加載的絕對路徑 let absPathname = path.resolve(__dirname, modulePath); // 創(chuàng)建模塊,新建Module實例 const module = new Module(absPathname); // 加載當前模塊 tryModuleLoad(module); // 返回exports對象 return module.exports; }
Module的實現(xiàn)很簡單,就是給模塊創(chuàng)建一個exports對象,tryModuleLoad執(zhí)行的時候將內(nèi)容加入到exports中,id就是模塊的絕對路徑。
// 定義模塊, 添加文件id標識和exports屬性 function Module(id) { this.id = id; // 讀取到的文件內(nèi)容會放在exports中 this.exports = {}; }
之前我們說過node模塊是運行在一個函數(shù)中,這里我們給Module掛載靜態(tài)屬性wrapper,里面定義一下這個函數(shù)的字符串,wrapper是一個數(shù)組,數(shù)組的第一個元素就是函數(shù)的參數(shù)部分,其中有exports,module. Require,__dirname, __filename, 都是我們模塊中常用的全局變量。注意這里傳入的Require參數(shù)是我們自己定義的Require。
第二個參數(shù)就是函數(shù)的結束部分。兩部分都是字符串,使用的時候我們將他們包裹在模塊的字符串外部就可以了。
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來包裹一下就相當于在這個模塊外部又包裹了一個函數(shù),也就實現(xiàn)了私有作用域。
使用call來執(zhí)行fn函數(shù),第一個參數(shù)改變運行的this我們傳入module.exports,后面的參數(shù)就是函數(shù)外面包裹參數(shù)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函數(shù)接收的是模塊對象,通過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實例化的方式創(chuàng)建module對象,將模塊的絕對路徑存儲在module的id屬性中,在module中創(chuàng)建exports屬性為一個json對象。
使用tryModuleLoad方法去加載模塊,tryModuleLoad中使用path.extname獲取到文件的擴展名,然后根據(jù)擴展名來執(zhí)行對應的模塊加載機制。
最終將加載到的模塊掛載module.exports中。tryModuleLoad執(zhí)行完畢之后module.exports已經(jīng)存在了,直接返回就可以了。
// 導入依賴 const path = require('path'); // 路徑操作 const fs = require('fs'); // 文件讀取 const vm = require('vm'); // 文件執(zhí)行 // 定義導入類,參數(shù)為模塊路徑 function Require(modulePath) { // 獲取當前要加載的絕對路徑 let absPathname = path.resolve(__dirname, modulePath); // 創(chuàng)建模塊,新建Module實例 const module = new Module(absPathname); // 加載當前模塊 tryModuleLoad(module); // 返回exports對象 return module.exports; } // 定義模塊, 添加文件id標識和exports屬性 function Module(id) { this.id = id; // 讀取到的文件內(nèi)容會放在exports中 this.exports = {}; } // 定義包裹模塊內(nèi)容的函數(shù) Module.wrapper = [ "(function(exports, module, Require, __dirname, __filename) {", "})" ] // 定義擴展名,不同的擴展名,加載方式不同,實現(xiàn)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.給模塊添加緩存
添加緩存也比較簡單,就是文件加載的時候將文件放入緩存在,再去加載模塊時先看緩存中是否存在,如果存在直接使用,如果不存在再去重新嘉愛,加載之后再放入緩存。
// 定義導入類,參數(shù)為模塊路徑 function Require(modulePath) { // 獲取當前要加載的絕對路徑 let absPathname = path.resolve(__dirname, modulePath); // 從緩存中讀取,如果存在,直接返回結果 if (Module._cache[absPathname]) { return Module._cache[absPathname].exports; } // 嘗試加載當前模塊 tryModuleLoad(module); // 創(chuàng)建模塊,新建Module實例 const module = new Module(absPathname); // 添加緩存 Module._cache[absPathname] = module; // 加載當前模塊 tryModuleLoad(module); // 返回exports對象 return module.exports; }
6.自動補全路徑
自動給模塊添加后綴名,實現(xiàn)省略后綴名加載模塊,其實也就是如果文件沒有后綴名的時候遍歷一下所有的后綴名看一下文件是否存在。
// 定義導入類,參數(shù)為模塊路徑 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); // 創(chuàng)建模塊,新建Module實例 const module = new Module(absPathname); // 添加緩存 Module._cache[absPathname] = module; // 加載當前模塊 tryModuleLoad(module); // 返回exports對象 return module.exports; }
7.分析實現(xiàn)步驟
1.導入相關模塊,創(chuàng)建一個Require方法。
2.抽離通過Module._load方法,用于加載模塊。
3.Module.resolveFilename 根據(jù)相對路徑,轉換成絕對路徑。
4.緩存模塊 Module._cache,同一個模塊不要重復加載,提升性能。
5.創(chuàng)建模塊 id: 保存的內(nèi)容是 exports = {}相當于this。
6.利用tryModuleLoad(module, filename) 嘗試加載模塊。
7.Module._extensions使用讀取文件。
8.Module.wrap: 把讀取到的js包裹一個函數(shù)。
9.將拿到的字符串使用runInThisContext運行字符串。
10.讓字符串執(zhí)行并將this改編成exports。
總結
到此這篇關于require加載器實現(xiàn)原理的文章就介紹到這了,更多相關require加載器原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
nodejs使用socket5進行代理請求的實現(xiàn)
這篇文章主要介紹了nodejs使用socket5進行代理請求的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02利用nodeJs anywhere搭建本地服務器環(huán)境的方法
今天小編就為大家分享一篇利用nodeJs anywhere搭建本地服務器環(huán)境的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-05-05