1. 程式人生 > 其它 >學習筆記—Node中require的實現

學習筆記—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 方法原始碼,其實現思路主要為以下幾點。

  1. require 方法的是 Module 模組的原型方法,也就是 Module.prototype.require
  2. 通過 Module._resolveFilename 方法,將傳入的路徑轉換為絕對路徑。並新增檔案的字尾名。(.js、.json 等)
  3. new Module 拿到轉換完畢的絕對路徑,並創造一個模組並匯出。(其中包含一個屬性id [ 當前檔案路徑 ],還有一個 exports
  4. Module.load 對模組進行載入。
  5. 根據檔案字尾 Module._extensions['.js'] 去做策略載入。
  6. fs.readFileSync 同步讀取檔案。
  7. 增加了一個函式的外殼 ( wrapper包裝 ) 讓這個函式執行,並且讓 Module.exports 作為當前上下文的 this
  8. 終端使用者會拿到 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;
}

現在我們來進行一步一步的解析。

  1. 首先,我們需要先引入需要用到的內建模組(fspathvm)。

    自定義一個 函式方法 _require ,這就是我們最終需要實現的方法,第一個引數接受傳入的路徑。

    const fs = require('fs');
    const path = require('path');
    const vm = require('vm');
    function _require(filename) {
    	// ...
    }
    
  2. 隨後我們還需要一個 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); // 絕對路徑
    }
    
  3. 我們發現目前列印的結果是沒有後綴的(不確定使用者是否填寫字尾),所以我們需要使用 fs.existsSync 判斷當前路徑是否存在。

    Module._resolveFilename = function (id) {
      // ...
      let isExists = fs.existsSync(filePath);
      if (isExists) return filePath;
    }
    

    如果存在,則直接返回結果。如果不存在,我們就需要給當前路徑嘗試新增字尾。

  4. 這裡我們就需要新增字尾,我們需要先定義一個 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')
      }
    }	
    

    這樣就可以保證我們 傳入的路徑,無論是加字尾或者不加字尾,都會返回一個 當前路徑的絕對路徑,且 一定會找到當前檔案

  5. 我們就已經建立好了一個 絕對引用路徑,方便我們後續進行讀取。

    現在我們就需要根據這個路徑,建立一個可以匯出的模組。

    這個模組就屬於 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;
    }
    

    這樣我們路徑和匯出的架子就有了,現在我們需要對中間部分進行處理。

  6. 其實所謂的 中間部分,也就是讓使用者對 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 方法。

  7. 我們先來完善一下 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]

  8. 在實現 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包裹的程式碼字串

  9. 現在我們來將字串變成可以執行的函式。使用 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 方法的整體思路就實現了。

    但是我們還有一個小問題,就是如果我們多次引入檔案,是沒有快取的。所以我們需要 對結果進行快取

  10. 定義一個 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 方法。

通過閱讀原始碼,並通過原始碼手寫方法,可以使我們更好的使用方法,也可以提升我們的技術。

本篇文章由莫小尚創作,文章中如有任何問題和紕漏,歡迎您的指正與交流。
您也可以關注我的 個人站點部落格園掘金,我會在文章產出後同步上傳到這些平臺上。
最後感謝您的支援!