Node.js深入學習之淺析require函數中怎麼新增勾點

2022-02-09 22:00:34
如何為 的 require 函數新增勾點?下面本篇文章就來帶大家瞭解一下require函數中新增勾點的方法,希望對大家有所幫助!

是一個基於 Chrome V8 引擎的 JavaScript 執行時環境。早期的 Node.js 採用的是 CommonJS 模組規範,從 Node v13.2.0 版本開始正式支援 ES Modules 特性。直到 v15.3.0 版本 ES Modules 特性才穩定下來並與 NPM 生態相相容。

1.png

本文將介紹 Node.js 中 require 函數的工作流程、如何讓 Node.js 直接執行 ts 檔案及如何正確地劫持 Node.js 的 require 函數,從而實現勾點的功能。接下來,我們先來介紹 require 函數。

require 函數

Node.js 應用由模組組成,每個檔案就是一個模組。對於 CommonJS 模組規範來說,我們通過 require 函數來匯入模組。那麼當我們使用 require 函數來匯入模組的時候,該函數內部發生了什麼?這裡我們通過呼叫堆疊來了解一下 require 的過程:

2.png

由上圖可知,在使用 require 匯入模組時,會呼叫 Module 物件的 load 方法來載入模組,該方法的實現如下所示:

// lib/internal/modules/cjs/loader.js
Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  const extension = findLongestRegisteredExtension(filename);

  Module._extensions[extension](this, filename);
  this.loaded = true;
  // 省略部分程式碼
};

注意:本文所參照 Node.js 原始碼所對應的版本是 v16.13.1

在以上程式碼中,重要的兩個步驟是:

  • 步驟一:根據檔名找出擴充套件名;
  • 步驟二:通過解析後的擴充套件名,在 Module._extensions 物件中查詢匹配的載入器。

在 Node.js 中內建了 3 種不同的載入器,用於載入 nodejsonjs 檔案。node 檔案載入器

// lib/internal/modules/cjs/loader.js
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path.toNamespacedPath(filename));
};

json 檔案載入器

// lib/internal/modules/cjs/loader.js
Module._extensions['.json'] = function(module, filename) {
 const content = fs.readFileSync(filename, 'utf8');
 try {
    module.exports = JSONParse(stripBOM(content));
 } catch (err) {
   err.message = filename + ': ' + err.message;
   throw err;
 }
};

js 檔案載入器

// lib/internal/modules/cjs/loader.js
Module._extensions['.js'] = function(module, filename) {
  // If already analyzed the source, then it will be cached.
  const cached = cjsParseCache.get(module);
  let content;
  if (cached?.source) {
    content = cached.source;
    cached.source = undefined;
  } else {
    content = fs.readFileSync(filename, 'utf8');
  }
  // 省略部分程式碼
  module._compile(content, filename);
};

下面我們來分析比較重要的 js 檔案載入器。通過觀察以上程式碼,我們可知 js 載入器的核心處理流程,也可以分為兩個步驟:

  • 步驟一:使用 fs.readFileSync 方法載入 js 檔案的內容;
  • 步驟二:使用 module._compile 方法編譯已載入的 js 程式碼。

那麼瞭解以上的知識之後,對我們有什麼用處呢?其實在瞭解 require 函數的工作流程之後,我們就可以擴充套件 Node.js 的載入器。比如讓 Node.js 能夠執行 ts 檔案。

// register.js
const fs = require("fs");
const Module = require("module");
const { transformSync } = require("esbuild");

Module._extensions[".ts"] = function (module, filename) {
  const content = fs.readFileSync(filename, "utf8");
  const { code } = transformSync(content, {
    sourcefile: filename,
    sourcemap: "both",
    loader: "ts",
    format: "cjs",
  });
  module._compile(code, filename);
};

在以上程式碼中,我們引入了內建的 module 模組,然後利用該模組的 _extensions 物件來註冊我們的自定義 ts 載入器。

其實,載入器的本質就是一個函數,在該函數內部我們利用 esbuild 模組提供的 transformSync API 來實現 ts -> js 程式碼的轉換。當完成程式碼轉換之後,會呼叫 module._compile 方法對程式碼進行編譯操作。

看到這裡相信有的小夥伴,也想到了 Webpack 中對應的 loader,想深入學習的話,可以閱讀 多圖詳解,一次性搞懂Webpack Loader 這篇文章。

地址:https://mp.weixin.qq.com/s/2v1uhw2j7yKsb1U5KE2qJA

篇幅有限,具體的編譯過程,我們就不展開介紹了。下面我們來看一下如何讓自定義的 ts 載入器生效。要讓 Node.js 能夠執行 ts 程式碼,我們就需要在執行 ts 程式碼前,先完成自定義 ts 載入器的註冊操作。慶幸的是,Node.js 為我們提供了模組的預載入機制:

 $ node --help | grep preload
   -r, --require=... module to preload (option can be repeated)

即利用 -r, --require 命令列設定項,我們就可以預載入指定的模組。瞭解完相關知識之後,我們來測試一下自定義 ts 載入器。首先建立一個 index.ts 檔案並輸入以下內容:

// index.ts
const add = (a: number, b: number) => a + b;

console.log("add(a, b) = ", add(3, 5));

然後在命令列輸入以下命令:

$ node -r ./register.js index.ts

當以上命令成功執行之後,控制檯會輸出以下內容:

add(a, b) =  8

很明顯我們自定義的 ts 檔案載入器生效了,這種擴充套件機制還是值得我們學習的。另外,需要注意的是在 load 方法中,findLongestRegisteredExtension 函數會判斷檔案的擴充套件名是否已經註冊在 Module._extensions 物件中,若未註冊的話,預設會返回 .js 字串。

// lib/internal/modules/cjs/loader.js
Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  const extension = findLongestRegisteredExtension(filename);

  Module._extensions[extension](this, filename);
  this.loaded = true;
  // 省略部分程式碼
};

這就意味著只要檔案中包含有效的 js 程式碼,require 函數就能正常載入它。比如下面的 a.txt 檔案:

  module.exports = "hello world";

看到這裡相信你已經瞭解 require 函數是如何載入模組及如何自定義 Node.js 檔案載入器。那麼,讓 Node.js 支援載入 tspngcss 等其它型別的檔案,有更優雅、更簡單的方案麼?答案是有的,我們可以使用 pirates 這個第三方庫。

pirates 是什麼

pirates 這個庫讓我們可以正確地劫持 Node.js 的 require 函數。利用這個庫,我們就可以很容易擴充套件 Node.js 載入器的功能。

pirates 的用法

你可以使用 npm 來安裝 pirates:

npm install --save pirates

在成功安裝 pirates 這個庫之後,就可以利用該模組匯出提供的 addHook 函數來新增勾點:

// register.js
const addHook = require("pirates").addHook;

const revert = addHook(
  (code, filename) => code.replace("@@foo", "console.log('foo');"),
  { exts: [".js"] }
);

需要注意的是呼叫 addHook 之後會返回一個 revert 函數,用於取消對 require 函數的劫持操作。下面我們來驗證一下 pirates 這個庫是否能正常工作,首先新建一個 index.js 檔案並輸入以下內容:

// index.js
console.log("@@foo")

然後在命令列輸入以下命令:

$ node -r ./register.js index.js

當以上命令成功執行之後,控制檯會輸出以下內容:

console.log('foo');

觀察以上結果可知,我們通過 addHook 函數新增的勾點生效了。是不是覺得挺神奇的,接下來我們來分析一下 pirates 的工作原理。

pirates 是如何工作的

pirates 底層是利用 Node.js 內建 module 模組提供的擴充套件機制來實現 Hook 功能。前面我們已經介紹過了,當使用 require 函數來載入模組時,Node.js 會根據檔案的字尾名來匹配對應的載入器。 其實 pirates 的原始碼並不會複雜,我們來重點分析 addHook 函數的核心處理邏輯:

// src/index.js
export function addHook(hook, opts = {}) {
  let reverted = false;
  const loaders = []; // 存放新的loader
  const oldLoaders = []; // 存放舊的loader
  let exts;

  const originalJSLoader = Module._extensions['.js']; // 原始的JS Loader 

  const matcher = opts.matcher || null;
  const ignoreNodeModules = opts.ignoreNodeModules !== false;
  exts = opts.extensions || opts.exts || opts.extension || opts.ext 
    || ['.js'];
  if (!Array.isArray(exts)) {
    exts = [exts];
  }
  exts.forEach((ext) { 
    // ... 
  }
}

為了提高執行效率,addHook 函數提供了 matcherignoreNodeModules 設定項來實現檔案過濾操作。在獲取到 exts 擴充套件名列表之後,就會使用新的載入器來替換已有的載入器。

exts.forEach((ext) => {
    if (typeof ext !== 'string') {
      throw new TypeError(`Invalid Extension: ${ext}`);
    }
    // 獲取已註冊的loader,若未找到,則預設使用JS Loader
    const oldLoader = Module._extensions[ext] || originalJSLoader;
    oldLoaders[ext] = Module._extensions[ext];

    loaders[ext] = Module._extensions[ext] = function newLoader(
	  mod, filename) {
      let compile;
      if (!reverted) {
        if (shouldCompile(filename, exts, matcher, ignoreNodeModules)) {
          compile = mod._compile;
          mod._compile = function _compile(code) {
			// 這裡需要恢復成原來的_compile函數,否則會出現死迴圈
            mod._compile = compile;
			// 在編譯前先執行使用者自定義的hook函數
            const newCode = hook(code, filename);
            if (typeof newCode !== 'string') {
              throw new Error(HOOK_RETURNED_NOTHING_ERROR_MESSAGE);
            }

            return mod._compile(newCode, filename);
          };
        }
      }

      oldLoader(mod, filename);
    };
});

觀察以上程式碼可知,在 addHook 函數內部是通過替換 mod._compile 方法來實現勾點的功能。即在呼叫原始的 mod._compile 方法進行編譯前,會先呼叫 hook(code, filename) 函數來執行使用者自定義的 hook 函數,從而對程式碼進行處理。

好的,至此本文的主要內容都介紹完了,在實際工作中,如果你想讓 Node.js 直接執行 ts 檔案,可以利用 ts-nodeesbuild-register 這兩個庫。其中 esbuild-register 這個庫內部就是使用了 pirates 提供的 Hook 機制來實現對應的功能。

更多node相關知識,請存取:!

以上就是Node.js深入學習之淺析require函數中怎麼新增勾點的詳細內容,更多請關注TW511.COM其它相關文章!