javascript
原生js已载入就执行函数_手写CommonJS 中的 require函数
前言
來自于圣松大佬的文章《手寫CommonJS 中的 require函數》
什么是 CommonJS ?
node.js 的應用采用的commonjs模塊規范。
每一個文件就是一個模塊,擁有自己獨立的作用域,變量,以及方法等,對其他的模塊都不可見。CommonJS規范規定:每個模塊內部,module變量代表當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的module.exports屬性。require方法用于加載模塊。
CommonJS模塊的特點:
所有代碼都運行在模塊作用域,不會污染全局作用域。
模塊可以多次加載,但是只會在第一次加載時運行一次,然后運行結果就被緩存了,以后再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現的順序。
如何使用?
假設我們現在有個a.js文件,我們要在main.js 中使用a.js的一些方法和變量,運行環境是nodejs。這樣我們就可以使用CommonJS規范,讓a文件導出方法/變量。然后使用require函數引入變量/函數。
示例:
//?a.jsmodule.exports?=?'這是a.js的變量';?//?導出一個變量/方法/對象都可以
//?main.js
let?str?=?require('./a');?//?這里如果導入a.js,那么他會自動按照預定順序幫你添加后綴
console.log(str);?//?輸出:'這是a.js的變量'
手寫一個require函數
前言
我們現在就開始手寫一個 精簡版的 require函數,這個require函數支持以下功能:
導入一個符合CommonJS規范的JS文件。支持自動添加文件后綴(暫時支持JS和JSON文件) 現在就開始吧!
1. 定義一個req方法
我們先自定義一個req方法,和全局的require函數隔離開。這個req方法,接受一個名為ID的參數,也就是要加載的文件路徑。
//?main.jsfunction?req(id){}
let?a?=?req('./a')
console.log(a)
2. 新建一個Module 類
新建一個module類,這個module將會處理文件加載的全過程。
function?Module(id)?{????this.id?=?id;?//?當前模塊的文件路徑
????this.exports?=?{}?//?當前模塊導出的結果,默認為空
}
3. 獲取文件絕對路徑
剛才我們介紹到,require 函數支持傳入一個路徑。這個路徑可以是相對路徑,也可以是絕對路徑,也可以不寫文件后綴名。
我們在Module類上添加一個叫做“_resolveFilename”的方法,用于解析用戶傳進去的文件路徑,獲取一個絕對路徑。
//?將一個相對路徑?轉化成絕對路徑Module._resolveFilename?=?function?(id)?{}
繼續添加一個 “extennsions” 的屬性,這個屬性是一個對象。key是文件擴展名,value就是擴展名對應的不同文件的處理方法。
我們通過debugger nodejs require源碼看到,原生的require函數支持四種類型文件:
js文件json文件
node文件
mjs文件
由于篇幅,這里我們就只支持兩個擴展名:.js 和.json。
我們分別在extensions對象上,添加兩個屬性,兩個屬性的值分別都是一個函數。方便不同文件類型分類處理。
//?main.js?Module.extensions['.js']?=?function?(module)?{}
Module.extensions['.json']?=?function?(module)?{}
接著,我們導入nodejs原生的“path”模塊和“fs”模塊,方便我們獲取文件絕對路徑和文件操作。
我們處理一下 Module._resolveFilename 這個方法,讓他可以正常工作。
Module._resolveFilename?=?function?(id)?{????//?將相對路徑轉化成絕對路徑
????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('文件不存在')
}
在這里,我們支持接受一個名id的參數,這個參數將是用戶傳來的路徑。
首先我們先使用 path.resolve()獲取到文件絕對路徑。接著用 fs.existsSync 判斷文件是否存在。如果沒有存在,我們就嘗試添加文件后綴。
我們會去遍歷現在支持的文件擴展對象,嘗試拼接路徑。如果拼接后文件存在,返回文件路徑。不存在拋出異常。
這樣我們在req方法內,就可以獲取到完整的文件路徑:
function?req(id){????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
}
4. 加載模塊 —— JS的實現
這里就是我們的重頭戲,加載common.js模塊。
首先 new 一個Module實例。傳入一個文件路徑,然后返回一個新的module實例。
接著定義一個 tryModuleLoad 函數,傳入我們新建立的module實例。
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?一個新模塊
????tryModuleLoad(module);?
}
tryModuleLoad 函數 獲取到module后,會使用 path.extname 函數獲取文件擴展名,接著按照不同擴展名交給不同的函數分別處理。
處理js文件加載.
第一步,傳入一個module對象實例。
使用module對象中的id屬性,獲取文件絕對路徑。拿到文件絕對路徑后,使用fs模塊讀取文件內容。讀取編碼是utf8。
Module.extensions['.js'] = function (module) { // 1) 讀取 let script = fs.readFileSync(module.id, 'utf8'); }
第二步,偽造一個自執行函數。
這里先新建一個wrapper 數組。數組的第0項是自執行函數開頭,最后一項是結尾。
let?wrapper?=?[????'(function?(exports,?require,?module,?__dirname,?__filename)?{\r\n',
????'\r\n})'
];
這個自執行函數需要傳入5個參數:exports對象,require函數,module對象,dirname路徑,fileame文件名。
我們將獲取到的要加載文件的內容,和自執行函數模版拼接,組裝成一個完整的可執行js文本:
Module.extensions['.js']?=?function?(module)?{????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?內容拼接
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
}
第三步:創建沙箱執行環境
這里我們就要用到nodejs中的 “vm” 模塊了。這個模塊可以創建一個nodejs的虛擬機,提供一個獨立的沙箱運行環境。
具體介紹可以看:vm模塊的官方介紹
我們使用vm模塊的 runInThisContext函數,他可以建立一個有全局global屬性的沙盒。用法是傳入一個js文本內容。我們將剛才拼接的文本內容傳入,返回一個fn函數:
const?vm?=?require('vm');Module.extensions['.js']?=?function?(module)?{
????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?內容拼接
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
????//?3)創建沙盒環境,返回js函數
????let?fn?=?vm.runInThisContext(content);?
}
第四步:執行沙箱環境,獲得導出對象。
因為我們上面有需要文件目錄路徑,所以我們先獲取一下目錄路徑。這里使用path模塊的dirname 方法。
接著我們使用call方法,傳入參數,立即執行。
call 方法的第一個參數是函數內部的this對象,其余參數都是函數所需要的參數。
Module.extensions['.js']?=?function?(module)?{????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?增加函數?還是一個字符串
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
????//?3)?讓這個字符串函數執行?(node里api)
????let?fn?=?vm.runInThisContext(content);?//?這里就會返回一個js函數
????let?__dirname?=?path.dirname(module.id);
????//?讓函數執行
????fn.call(module.exports,?module.exports,?req,?module,?__dirname,?module.id)
}
這樣,我們傳入module對象,接著內部會將要導出的值掛在到module的export屬性上。
第五步:返回導出值
由于我們的處理函數是非純函數,所以直接返回module實例的export對象就ok。
function?req(id){?//?沒有異步的api方法????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
????tryModuleLoad(module);?//?module.exports?=?{}
????return?module.exports;
}
這樣,我們就實現了一個簡單的require函數。
let?str?=?req('./a');//?str?=?req('./a');
console.log(str);
//?a.js
module.exports?=?"這是a.js文件"
5. 加載模塊 —— JSON文件的實現
json文件的實現就比較簡單了。使用fs讀取json文件內容,然后用JSON.parse轉為js對象就ok。
Module.extensions['.json']?=?function?(module)?{????let?script?=?fs.readFileSync(module.id,?'utf8');
????module.exports?=?JSON.parse(script)
}
6. 優化
文章初,我們有寫:commonjs會將我們要加載的模塊緩存。等我們再次讀取時,就去緩存中讀取我們的模塊,而不是再次調用fs和vm模塊獲得導出內容。
我們在Module對象上新建一個_cache屬性。這個屬性是一個對象,key是文件名,value是文件導出的內容緩存。
在我們加載模塊時,首先先去_cache屬性上找有沒有緩存過。如果有,直接返回緩存內容。如果沒有,嘗試獲取導出內容,并掛在到緩存對象上。
Module._cache?=?{}function?req(id){
????//?通過相對路徑獲取絕對路徑
????let?filename?=?Module._resolveFilename(id);
????let?cache?=?Module._cache[filename];
????if(cache){?//?如果有緩存,直接將模塊的結果返回
????????return?cache.exports
????}
????let?module?=?new?Module(filename);?//?創建了一個模塊實例
????Module._cache[filename]?=?module?//?輸入進緩存對象內
????//?加載相關模塊?(就是給這個模塊的exports賦值)
????tryModuleLoad(module);?//?module.exports?=?{}
????return?module.exports;
}
完整實現
const?path?=?require('path');const?fs?=?require('fs');
const?vm?=?require('vm');
function?Module(id)?{
????this.id?=?id;?//?當前模塊的id名
????this.exports?=?{};?//?默認是空對象?導出的結果
}
Module.extensions =?{};
//?如果文件是js?的話?后期用這個函數來處理
Module.extensions['.js']?=?function?(module)?{
????//?1)?讀取
????let?script?=?fs.readFileSync(module.id,?'utf8');
????//?2)?增加函數?還是一個字符串
????let?content?=?wrapper[0]?+?script?+?wrapper[1];
????//?3)?讓這個字符串函數執行?(node里api)
????let?fn?=?vm.runInThisContext(content);?//?這里就會返回一個js函數
????let?__dirname?=?path.dirname(module.id);
????//?讓函數執行
????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)
}
//?將一個相對路徑?轉化成絕對路徑
Module._resolveFilename?=?function?(id)?{
????//?將相對路徑轉化成絕對路徑
????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})'
];
//?模塊獨立?相互沒關系
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){?//?如果有緩存直接將模塊的結果返回
????????return?cache.exports
????}
????let?module?=?new?Module(filename);?//?創建了一個模塊
????Module._cache[filename]?=?module;
????//?加載相關模塊?(就是給這個模塊的exports賦值)
????tryModuleLoad(module);?//?module.exports?=?{}
????return?module.exports;
}
let?str?=?req('./a');
console.log(str);
結束總結
這樣,我們就手寫實現了一個精簡版的CommonJS require函數。
讓我們回顧一下,require的實現流程:
- 拿到要加載的文件絕對路徑。沒有后綴的嘗試添加后綴
- 嘗試從緩存中讀取導出內容。如果緩存有,返回緩存內容。沒有,下一步處理
- 新建一個模塊實例,并輸入進緩存對象
- 嘗試加載模塊
- 根據文件類型,分類處理
- 如果是js文件,讀取到文件內容,拼接自執行函數文本,用vm模塊創建沙箱實例加載函數文本,獲得導出內容,返回內容
- 如果是json文件,讀取到文件內容,用JSON.parse 函數轉成js對象,返回內容 獲取導出返回值。
掛個招聘
我們是碼云Gitee私有化部門,正在招聘阿里p6級別的前端開發。要求:統招本科學歷及以上,4年以上前端開發經驗,25-35k。坐標北京西三旗,不打卡,不996。有意者請發送簡歷至:wangshengsong@oschina.cn
在線筆記
最近花了點時間把筆記整理到語雀上了,方便同學們閱讀:公眾號回復筆記或者簡歷
最后
1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創作的動力。
2.關注公眾號前端壹棧,回復「1」加入前端交流群!「在這里有好多前端開發者,會討論前端知識,互相學習」!
3.也可添加公眾號【前端壹棧】,一起成長
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的原生js已载入就执行函数_手写CommonJS 中的 require函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c#web页面显示弹窗_C#中三种弹出信
- 下一篇: jieba库词频统计_网购评论之词频分析