node.js 的應用採用的commonjs模塊規範。javascript
每個文件就是一個模塊,擁有本身獨立的做用域,變量,以及方法等,對其餘的模塊都不可見。CommonJS規範規定:每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。require方法用於加載模塊。html
全部代碼都運行在模塊做用域,不會污染全局做用域。前端
模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。java
模塊加載的順序,按照其在代碼中出現的順序。node
假設咱們如今有個a.js文件,咱們要在main.js 中使用a.js的一些方法和變量,運行環境是nodejs。這樣咱們就可使用CommonJS規範,讓a文件導出方法/變量。而後使用require函數引入變量/函數。json
示例:api
// a.js
module.exports = '這是a.js的變量'; // 導出一個變量/方法/對象均可以複製代碼
// main.js
let str = require('./a'); // 這裏若是導入a.js,那麼他會自動按照預約順序幫你添加後綴
console.log(str); // 輸出:'這是a.js的變量'複製代碼
咱們如今就開始手寫一個 精簡版的 require函數,這個require函數支持如下功能:數組
如今就開始吧!緩存
咱們先自定義一個req方法,和全局的require函數隔離開。bash
這個req方法,接受一個名爲ID的參數,也就是要加載的文件路徑。
// main.js
function req(id){}
let a = req('./a')
console.log(a)複製代碼
新建一個module類,這個module將會處理文件加載的全過程。
function Module(id) {
this.id = id; // 當前模塊的文件路徑
this.exports = {} // 當前模塊導出的結果,默認爲空
}
複製代碼
剛纔咱們介紹到,require 函數支持傳入一個路徑。這個路徑能夠是相對路徑,也能夠是絕對路徑,也能夠不寫文件後綴名。
咱們在Module類上添加一個叫作「_resolveFilename」 的方法,用於解析用戶傳進去的文件路徑,獲取一個絕對路徑。
// 將一個相對路徑 轉化成絕對路徑
Module._resolveFilename = function (id) {}複製代碼
繼續添加一個 「extennsions」 的屬性,這個屬性是一個對象。key是文件擴展名,value就是擴展名對應的不一樣文件的處理方法。
咱們經過debugger nodejs require源碼看到,原生的require函數支持四種類型文件:
因爲篇幅,這裏咱們就只支持兩個擴展名:.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 < extenisons.length; 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);
}複製代碼
這裏就是咱們的重頭戲,加載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文件"複製代碼
json文件的實現就比較簡單了。使用fs讀取json文件內容,而後用JSON.parse轉爲js對象就ok。
Module.extensions['.json'] = function (module) {
let script = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(script)
}複製代碼
文章初,咱們有寫: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 < extenisons.length; 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的實現流程:
咱們是碼雲Gitee私有化部門,正在招聘阿里p6級別的前端開發。要求:統招本科學歷及以上,4年以上前端開發經驗,25-35k。座標北京西三旗,不打卡,不996。有意者請發送簡歷至:wangshengsong@oschina.cn