【JS】797- 手寫CommonJS 中的 require函數(shù)

前言
來自于圣松大佬的文章
《手寫CommonJS 中的 require函數(shù)》
什么是 CommonJS ?
node.js 的應(yīng)用采用的commonjs模塊規(guī)范。
每一個(gè)文件就是一個(gè)模塊,擁有自己獨(dú)立的作用域,變量,以及方法等,對其他的模塊都不可見。CommonJS規(guī)范規(guī)定:每個(gè)模塊內(nèi)部,module變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對象,它的exports屬性(即module.exports)是對外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的module.exports屬性。require方法用于加載模塊。
CommonJS模塊的特點(diǎn):
所有代碼都運(yùn)行在模塊作用域,不會(huì)污染全局作用域。
模塊可以多次加載,但是只會(huì)在第一次加載時(shí)運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
如何使用?
假設(shè)我們現(xiàn)在有個(gè)a.js文件,我們要在main.js 中使用a.js的一些方法和變量,運(yùn)行環(huán)境是nodejs。這樣我們就可以使用CommonJS規(guī)范,讓a文件導(dǎo)出方法/變量。然后使用require函數(shù)引入變量/函數(shù)。
示例:
//?a.js
module.exports?=?'這是a.js的變量';?//?導(dǎo)出一個(gè)變量/方法/對象都可以
//?main.js
let?str?=?require('./a');?//?這里如果導(dǎo)入a.js,那么他會(huì)自動(dòng)按照預(yù)定順序幫你添加后綴
console.log(str);?//?輸出:'這是a.js的變量'
手寫一個(gè)require函數(shù)
前言
我們現(xiàn)在就開始手寫一個(gè) 精簡版的 require函數(shù),這個(gè)require函數(shù)支持以下功能:
導(dǎo)入一個(gè)符合CommonJS規(guī)范的JS文件。支持自動(dòng)添加文件后綴(暫時(shí)支持JS和JSON文件) 現(xiàn)在就開始吧!
1. 定義一個(gè)req方法
我們先自定義一個(gè)req方法,和全局的require函數(shù)隔離開。這個(gè)req方法,接受一個(gè)名為ID的參數(shù),也就是要加載的文件路徑。
//?main.js
function?req(id){}
let?a?=?req('./a')
console.log(a)
2. 新建一個(gè)Module 類
新建一個(gè)module類,這個(gè)module將會(huì)處理文件加載的全過程。
function?Module(id)?{
????this.id?=?id;?//?當(dāng)前模塊的文件路徑
????this.exports?=?{}?//?當(dāng)前模塊導(dǎo)出的結(jié)果,默認(rèn)為空
}
3. 獲取文件絕對路徑
剛才我們介紹到,require 函數(shù)支持傳入一個(gè)路徑。這個(gè)路徑可以是相對路徑,也可以是絕對路徑,也可以不寫文件后綴名。
我們在Module類上添加一個(gè)叫做“_resolveFilename”的方法,用于解析用戶傳進(jìn)去的文件路徑,獲取一個(gè)絕對路徑。
//?將一個(gè)相對路徑?轉(zhuǎn)化成絕對路徑
Module._resolveFilename?=?function?(id)?{}
繼續(xù)添加一個(gè) “extennsions” 的屬性,這個(gè)屬性是一個(gè)對象。key是文件擴(kuò)展名,value就是擴(kuò)展名對應(yīng)的不同文件的處理方法。
我們通過debugger nodejs require源碼看到,原生的require函數(shù)支持四種類型文件:
js文件
json文件
node文件
mjs文件
由于篇幅,這里我們就只支持兩個(gè)擴(kuò)展名:.js 和.json。
我們分別在extensions對象上,添加兩個(gè)屬性,兩個(gè)屬性的值分別都是一個(gè)函數(shù)。方便不同文件類型分類處理。
//?main.js?
Module.extensions['.js']?=?function?(module)?{}
Module.extensions['.json']?=?function?(module)?{}
接著,我們導(dǎo)入nodejs原生的“path”模塊和“fs”模塊,方便我們獲取文件絕對路徑和文件操作。
我們處理一下 Module._resolveFilename 這個(gè)方法,讓他可以正常工作。
Module._resolveFilename?=?function?(id)?{
????//?將相對路徑轉(zhuǎn)化成絕對路徑
????let?absPath?=?path.resolve(id);
????//??先判斷文件是否存在如果存在了就不要增加了?
????if(fs.existsSync(absPath)){
????????return?absPath;
????}
????//?去嘗試添加文件后綴?.js?.json?
????let?extenisons?=?Object.keys(Module.extensions);
????for?(let?i?=?0;?i?????????let?ext?=?extenisons[i];
????????//?判斷路徑是否存在
????????let?currentPath?=?absPath?+?ext;?//?獲取拼接后的路徑
????????let?exits?=?fs.existsSync(currentPath);?//?判斷是否存在
????????if(exits){
????????????return?currentPath
????????}
????}
????throw?new?Error('文件不存在')
}
在這里,我們支持接受一個(gè)名id的參數(shù),這個(gè)參數(shù)將是用戶傳來的路徑。
首先我們先使用 path.resolve()獲取到文件絕對路徑。接著用 fs.existsSync 判斷文件是否存在。如果沒有存在,我們就嘗試添加文件后綴。
我們會(huì)去遍歷現(xiàn)在支持的文件擴(kuò)展對象,嘗試拼接路徑。如果拼接后文件存在,返回文件路徑。不存在拋出異常。
這樣我們在req方法內(nèi),就可以獲取到完整的文件路徑:
function?req(id){
????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
}
4. 加載模塊 —— JS的實(shí)現(xiàn)
這里就是我們的重頭戲,加載common.js模塊。
首先 new 一個(gè)Module實(shí)例。傳入一個(gè)文件路徑,然后返回一個(gè)新的module實(shí)例。
接著定義一個(gè) tryModuleLoad 函數(shù),傳入我們新建立的module實(shí)例。
function?tryModuleLoad(module)?{?//?嘗試加載模塊
???let?ext?=?path.extname(module.id);
???Module.extensions[ext](module)
}
function?req(id){
????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
????let?module?=?new?Module(filename);?//?new?一個(gè)新模塊
????tryModuleLoad(module);?
}
tryModuleLoad 函數(shù) 獲取到module后,會(huì)使用 path.extname 函數(shù)獲取文件擴(kuò)展名,接著按照不同擴(kuò)展名交給不同的函數(shù)分別處理。
處理js文件加載.
第一步,傳入一個(gè)module對象實(shí)例。
使用module對象中的id屬性,獲取文件絕對路徑。拿到文件絕對路徑后,使用fs模塊讀取文件內(nèi)容。讀取編碼是utf8。
Module.extensions['.js'] = function (module) { // 1) 讀取 let script = fs.readFileSync(module.id, 'utf8'); }
第二步,偽造一個(gè)自執(zhí)行函數(shù)。
這里先新建一個(gè)wrapper 數(shù)組。數(shù)組的第0項(xiàng)是自執(zhí)行函數(shù)開頭,最后一項(xiàng)是結(jié)尾。
let?wrapper?=?[
????'(function?(exports,?require,?module,?__dirname,?__filename)?{\r\n',
????'\r\n})'
];
這個(gè)自執(zhí)行函數(shù)需要傳入5個(gè)參數(shù):exports對象,require函數(shù),module對象,dirname路徑,fileame文件名。
我們將獲取到的要加載文件的內(nèi)容,和自執(zhí)行函數(shù)模版拼接,組裝成一個(gè)完整的可執(zhí)行js文本:
Module.extensions['.js']?=?function?(module)?{
????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?內(nèi)容拼接
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
}
第三步:創(chuàng)建沙箱執(zhí)行環(huán)境
這里我們就要用到nodejs中的 “vm” 模塊了。這個(gè)模塊可以創(chuàng)建一個(gè)nodejs的虛擬機(jī),提供一個(gè)獨(dú)立的沙箱運(yùn)行環(huán)境。
具體介紹可以看:vm模塊的官方介紹
我們使用vm模塊的 runInThisContext函數(shù),他可以建立一個(gè)有全局global屬性的沙盒。用法是傳入一個(gè)js文本內(nèi)容。我們將剛才拼接的文本內(nèi)容傳入,返回一個(gè)fn函數(shù):
const?vm?=?require('vm');
Module.extensions['.js']?=?function?(module)?{
????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?內(nèi)容拼接
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
????//?3)創(chuàng)建沙盒環(huán)境,返回js函數(shù)
????let?fn?=?vm.runInThisContext(content);?
}
第四步:執(zhí)行沙箱環(huán)境,獲得導(dǎo)出對象。
因?yàn)槲覀兩厦嬗行枰募夸浡窂剑晕覀兿全@取一下目錄路徑。這里使用path模塊的dirname 方法。
接著我們使用call方法,傳入?yún)?shù),立即執(zhí)行。
call 方法的第一個(gè)參數(shù)是函數(shù)內(nèi)部的this對象,其余參數(shù)都是函數(shù)所需要的參數(shù)。
Module.extensions['.js']?=?function?(module)?{
????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?增加函數(shù)?還是一個(gè)字符串
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
????//?3)?讓這個(gè)字符串函數(shù)執(zhí)行?(node里api)
????let?fn?=?vm.runInThisContext(content);?//?這里就會(huì)返回一個(gè)js函數(shù)
????let?__dirname?=?path.dirname(module.id);
????//?讓函數(shù)執(zhí)行
????fn.call(module.exports,?module.exports,?req,?module,?__dirname,?module.id)
}
這樣,我們傳入module對象,接著內(nèi)部會(huì)將要導(dǎo)出的值掛在到module的export屬性上。
第五步:返回導(dǎo)出值
由于我們的處理函數(shù)是非純函數(shù),所以直接返回module實(shí)例的export對象就ok。
function?req(id){?//?沒有異步的api方法
????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
????tryModuleLoad(module);?//?module.exports?=?{}
????return?module.exports;
}
這樣,我們就實(shí)現(xiàn)了一個(gè)簡單的require函數(shù)。
let?str?=?req('./a');
//?str?=?req('./a');
console.log(str);
//?a.js
module.exports?=?"這是a.js文件"
5. 加載模塊 —— JSON文件的實(shí)現(xiàn)
json文件的實(shí)現(xiàn)就比較簡單了。使用fs讀取json文件內(nèi)容,然后用JSON.parse轉(zhuǎn)為js對象就ok。
Module.extensions['.json']?=?function?(module)?{
????let?script?=?fs.readFileSync(module.id,?'utf8');
????module.exports?=?JSON.parse(script)
}
6. 優(yōu)化
文章初,我們有寫:commonjs會(huì)將我們要加載的模塊緩存。等我們再次讀取時(shí),就去緩存中讀取我們的模塊,而不是再次調(diào)用fs和vm模塊獲得導(dǎo)出內(nèi)容。
我們在Module對象上新建一個(gè)_cache屬性。這個(gè)屬性是一個(gè)對象,key是文件名,value是文件導(dǎo)出的內(nèi)容緩存。
在我們加載模塊時(shí),首先先去_cache屬性上找有沒有緩存過。如果有,直接返回緩存內(nèi)容。如果沒有,嘗試獲取導(dǎo)出內(nèi)容,并掛在到緩存對象上。
Module._cache?=?{}
function?req(id){
????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
????let?cache?=?Module._cache[filename];
????if(cache){?//?如果有緩存,直接將模塊的結(jié)果返回
????????return?cache.exports
????}
????let?module?=?new?Module(filename);?//?創(chuàng)建了一個(gè)模塊實(shí)例
????Module._cache[filename]?=?module?//?輸入進(jìn)緩存對象內(nèi)
????//?加載相關(guān)模塊?(就是給這個(gè)模塊的exports賦值)
????tryModuleLoad(module);?//?module.exports?=?{}
????return?module.exports;
}
完整實(shí)現(xiàn)
const?path?=?require('path');
const?fs?=?require('fs');
const?vm?=?require('vm');
function?Module(id)?{
????this.id?=?id;?//?當(dāng)前模塊的id名
????this.exports?=?{};?//?默認(rèn)是空對象?導(dǎo)出的結(jié)果
}
Module.extensions =?{};
//?如果文件是js?的話?后期用這個(gè)函數(shù)來處理
Module.extensions['.js']?=?function?(module)?{
????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?增加函數(shù)?還是一個(gè)字符串
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
????//?3)?讓這個(gè)字符串函數(shù)執(zhí)行?(node里api)
????let?fn?=?vm.runInThisContext(content);?//?這里就會(huì)返回一個(gè)js函數(shù)
????let?__dirname?=?path.dirname(module.id);
????//?讓函數(shù)執(zhí)行
????fn.call(module.exports,?module.exports,?req,?module,?__dirname,?module.id)
}
//?如果文件是json
Module.extensions['.json']?=?function?(module)?{
????let?script?=?fs.readFileSync(module.id,?'utf8');
????module.exports?=?JSON.parse(script)
}
//?將一個(gè)相對路徑?轉(zhuǎn)化成絕對路徑
Module._resolveFilename?=?function?(id)?{
????//?將相對路徑轉(zhuǎn)化成絕對路徑
????let?absPath?=?path.resolve(id);
????//??先判斷文件是否存在如果存在
????if(fs.existsSync(absPath)){
????????return?absPath;
????}
????//?去嘗試添加文件后綴?.js?.json?
????let?extenisons?=?Object.keys(Module.extensions);
????for?(let?i?=?0;?i?????????let?ext?=?extenisons[i];
????????//?判斷路徑是否存在
????????let?currentPath?=?absPath?+?ext;?//?獲取拼接后的路徑
????????let?exits?=?fs.existsSync(currentPath);?//?判斷是否存在
????????if(exits){
????????????return?currentPath
????????}
????}
????throw?new?Error('文件不存在')
}
let?wrapper?=?[
????'(function?(exports,?require,?module,?__dirname,?__filename)?{\r\n',
????'\r\n})'
];
//?模塊獨(dú)立?相互沒關(guān)系
function?tryModuleLoad(module)?{?//?嘗試加載模塊
???let?ext?=??path.extname(module.id);
???Module.extensions[ext](module)
}
Module._cache?=?{}
function?req(id){?//?沒有異步的api方法
????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
????let?cache?=?Module._cache[filename];
????if(cache){?//?如果有緩存直接將模塊的結(jié)果返回
????????return?cache.exports
????}
????let?module?=?new?Module(filename);?//?創(chuàng)建了一個(gè)模塊
????Module._cache[filename]?=?module;
????//?加載相關(guān)模塊?(就是給這個(gè)模塊的exports賦值)
????tryModuleLoad(module);?//?module.exports?=?{}
????return?module.exports;
}
let?str?=?req('./a');
console.log(str);
結(jié)束總結(jié)
這樣,我們就手寫實(shí)現(xiàn)了一個(gè)精簡版的CommonJS require函數(shù)。
讓我們回顧一下,require的實(shí)現(xiàn)流程:
拿到要加載的文件絕對路徑。沒有后綴的嘗試添加后綴 嘗試從緩存中讀取導(dǎo)出內(nèi)容。如果緩存有,返回緩存內(nèi)容。沒有,下一步處理 新建一個(gè)模塊實(shí)例,并輸入進(jìn)緩存對象 嘗試加載模塊 根據(jù)文件類型,分類處理 如果是js文件,讀取到文件內(nèi)容,拼接自執(zhí)行函數(shù)文本,用vm模塊創(chuàng)建沙箱實(shí)例加載函數(shù)文本,獲得導(dǎo)出內(nèi)容,返回內(nèi)容 如果是json文件,讀取到文件內(nèi)容,用JSON.parse 函數(shù)轉(zhuǎn)成js對象,返回內(nèi)容 獲取導(dǎo)出返回值。

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
