學習筆記—Node中require的實現
日常的學習筆記,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,後續可能還會繼續更新 Typescript、Vue3 和 常見的面試題 等等。
require
在上一篇文章中,我們瞭解到瞭如何去通過 除錯檢視Node原始碼。
// 使用 require 引入檔案
// a.js
var a = 100;
module.exports = a;
// b.js
let a = require('./a')
console.log(a)
通過除錯檢視 require
方法原始碼,其實現思路主要為以下幾點。
require
方法的是Module
模組的原型方法,也就是Module.prototype.require
- 通過
Module._resolveFilename
方法,將傳入的路徑轉換為絕對路徑。並新增檔案的字尾名。(.js、.json 等) new Module
拿到轉換完畢的絕對路徑,並創造一個模組並匯出。(其中包含一個屬性id [ 當前檔案路徑 ],還有一個 exports)Module.load
對模組進行載入。- 根據檔案字尾
Module._extensions['.js']
去做策略載入。 fs.readFileSync
同步讀取檔案。- 增加了一個函式的外殼 ( wrapper包裝 ) 讓這個函式執行,並且讓
Module.exports
作為當前上下文的this
。 - 終端使用者會拿到
Module.exports
所以,最終會返回一個 Module.exports
物件。通過以上思路,我們就可以實現一套 require
方法。
實現require方法
根據上述規則,我們可以模擬實現一套 require
方法。
const fs = require('fs'); const path = require('path'); const vm = require('vm'); function Module(id) { this.id = id; this.exports = {}; } Module.wrapper = [ `(function(exports,require,module,__filename,__dirname){`, `})` ]; Module._extensions = { '.js'(module) { let content = fs.readFileSync(module.id, 'utf8'); content = Module.wrapper[0] + content + Module.wrapper[1]; let fn = vm.runInThisContext(content); let exports = module.exports; let dirname = path.dirname(module.id); fn.call(exports, exports, _require, module, module.id, dirname); }, '.json'(module) { let content = fs.readFileSync(module.id, 'utf8'); module.exports = JSON.parse(content); } } Module._resolveFilename = function (filename) { let filePath = path.resolve(__dirname, filename); let isExists = fs.existsSync(filePath); if (isExists) { return absPath; } else { let keys = Object.keys(Module._extensions); for (let i = 0; i < keys.length; i++) { let newPath = filePath + keys; if (fs.existsSync(newPath)) return newPath; } throw new Error('module not exists') } } Module.prototype.load = function () { let extName = path.extname(this.id); Module._extensions[extName](this); } Module._cache = {}; function _require(filename) { filename = Module._resolveFilename(filename); let cacheModule = Module._cache[filename]; if (cacheModule) { return cacheModule.exports; } let module = new Module(filename); Module._cache[filename] = module module.load(); return module.exports; }
現在我們來進行一步一步的解析。
-
首先,我們需要先引入需要用到的內建模組(
fs
、path
和vm
)。自定義一個 函式方法
_require
,這就是我們最終需要實現的方法,第一個引數接受傳入的路徑。const fs = require('fs'); const path = require('path'); const vm = require('vm'); function _require(filename) { // ... }
-
隨後我們還需要一個
Module._resolveFilename
方法將傳入的路徑轉換成絕對路徑,並新增字尾。因為使用
Module
方法,我們我們也需要宣告一個名為Module
的建構函式。隨後將路徑傳入
Module._resolveFilename
方法中。function Module() {} Module._resolveFilename = function (id) { let filePath = path.resolve(__dirname, id); console.log(filePath); // d:\xxx\xxx\xxx\a } function _require(filename) { filename = Module._resolveFilename(filename); // 絕對路徑 }
-
我們發現目前列印的結果是沒有後綴的(不確定使用者是否填寫字尾),所以我們需要使用
fs.existsSync
判斷當前路徑是否存在。Module._resolveFilename = function (id) { // ... let isExists = fs.existsSync(filePath); if (isExists) return filePath; }
如果存在,則直接返回結果。如果不存在,我們就需要給當前路徑嘗試新增字尾。
-
這裡我們就需要新增字尾,我們需要先定義一個
Module._extensions
方法來對字尾進行分類。Module._extensions = { '.js'() {}, '.json'() {} }
然後我們將定義的字尾方法的
keys
新增到路徑上,並再次進行路徑判斷。路徑存在則直接返回結果,如果路徑不存在,這次就需要 返回一個錯誤。
Module._resolveFilename = function (id) { if(isExists){ // ... } else { let keys = Object.keys(Module._extensions); for (let i = 0; i < keys.length; i++) { let newPath = filePath + keys; if (fs.existsSync(newPath)) return newPath; } throw new Error('module not exists') } }
這樣就可以保證我們 傳入的路徑,無論是加字尾或者不加字尾,都會返回一個 當前路徑的絕對路徑,且 一定會找到當前檔案。
-
我們就已經建立好了一個 絕對引用路徑,方便我們後續進行讀取。
現在我們就需要根據這個路徑,建立一個可以匯出的模組。
這個模組就屬於
Module
建構函式,根據我們一開始檢視原始碼時總結的定義,我們知道 模組全部都是通過Module.exports
方法進行匯出的。function Module(id) { this.id = id; // 絕對路徑 this.exports = {}; // 預設匯出的是空物件 } function _require(filename) { filename = Module._resolveFilename(filename); // 絕對路徑 let module = new Module(filename); return module.exports; }
這樣我們路徑和匯出的架子就有了,現在我們需要對中間部分進行處理。
-
其實所謂的 中間部分,也就是讓使用者對
Module.exports
賦值(目前匯出的是空物件)。根據原始碼的定義,我們需要定義一個
module.load
來對模組進行載入。Module.prototype.load = function () { let extName = path.extname(this.id); // 獲取字尾名 Module._extensions[extName](this); } function _require(filename) { // ... module.load(); }
這種定義的好處就是,我們可以根據傳入的字尾名,呼叫不同的處理策略。實現檔案的 策略載入。
這樣我們的
module
就會被傳到上面的Module._extensions
方法中Module._extensions = { '.js'(module) {}, '.json'(module) {} }
下一步我們就需要完善一下
Module._extensions
方法。 -
我們先來完善一下
json
方法,因為這個是最好實現的。我們先隨便定義一個
.json
檔案來進行測試。// a.json { "name" : "MXShang", "age" : 26 }
然後我們來完善一下
Module._extensions[json]
方法Module._extensions = { '.js'() {}, '.json'(module) { let content = fs.readFileSync(module.id, 'utf8') module.exports = JSON.parse(content); } }
獲取絕對路徑,通過
fs.readFileSync
同步讀取內容,並輸出。很好理解也很簡單,接下來我們看一下
Module._extensions[js]
。 -
在實現
Module._extensions[js]
方法前,我們先需要完成一個函式的外殼 ( wrapper包裝 ) ,也就是我們之前文章中經常提到的,包含五個引數的函式。Module.wrapper = [ `(function(exports,require,module,__filename,__dirname){`, `})` ];
然後我們來實現
Module._extensions[js]
方法。思路與實現
.json
相似,先將絕對路徑的內容讀出來,並將內容放到 wrapper 中。。Module._extensions = { '.js'(module) { let content = fs.readFileSync(module.id, 'utf8'); content = Module.wrapper[0] + content + Module.wrapper[1]; } }
這樣我們就可以得到一個 被wrapper包裹的程式碼字串。
-
現在我們來將字串變成可以執行的函式。使用
vm.runInThisContext
將字串變成函式。Module._extensions = { '.js'(module) { // ... let fn = vm.runInThisContext(content); // 獲取最終執行的函式 } }
現在我們需要明確一下
fn
的執行位置,也就是其 this的指向。不用多說,this一定是指向
module.exports
的,所以我們需要通過fn.call
方法來將函式的this指向當前建構函式。然後我們再根據最終的函式,依次獲取一下需要傳遞的五個引數。
Module._extensions = { '.js'(module) { // ... let exports = module.exports; // 當前this也就是exports引數。this = exports = module.exports let dirname = path.dirname(module.id); // 當前檔案執行位置的絕對略經 fn.call(exports, exports, _require, module, module.id, dirname); } }
到了這一步我們就可以發現,
require
方法實際上就是通過Module
作為一個 中間層 來實現的。至此,我們的
require
方法的整體思路就實現了。但是我們還有一個小問題,就是如果我們多次引入檔案,是沒有快取的。所以我們需要 對結果進行快取 。
-
定義一個
Module._cache
來對結果進行快取。// 定義一個 Module._cache Module._cache = {}; // 在模組載入前,先將定義好的module模組結果進行快取 function _require(filename) { // ... Module._cache[filename] = module; // 根據檔名進行快取 module.load(); return module.exports; }
如果當前模組已經被快取過 (載入過) ,直接將快取好的模組匯出就可以了。
function _require(filename) { // ... let cacheModule = Module._cache[filename]; if (cacheModule) { return cacheModule.exports; } let module = new Module(filename); Module._cache[filename] = module; // 根據檔名進行快取 module.load(); return module.exports; }
這樣我們就實現了一套
require
方法。
通過閱讀原始碼,並通過原始碼手寫方法,可以使我們更好的使用方法,也可以提升我們的技術。
本篇文章由莫小尚創作,文章中如有任何問題和紕漏,歡迎您的指正與交流。
您也可以關注我的 個人站點、部落格園 和 掘金,我會在文章產出後同步上傳到這些平臺上。
最後感謝您的支援!