Node.js的require函數(shù)中如何添加鉤子

本文小編為大家詳細介紹“Node.js的require函數(shù)中如何添加鉤子”,內(nèi)容詳細,步驟清晰,細節(jié)處理妥當(dāng),希望這篇“Node.js的require函數(shù)中如何添加鉤子”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

站在用戶的角度思考問題,與客戶深入溝通,找到蓮花網(wǎng)站設(shè)計與蓮花網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗,讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:做網(wǎng)站、網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣、域名注冊、雅安服務(wù)器托管、企業(yè)郵箱。業(yè)務(wù)覆蓋蓮花地區(qū)。

Node.js的require函數(shù)中如何添加鉤子

Node.js 是一個基于 Chrome V8 引擎的 JavaScript 運行時環(huán)境。早期的 Node.js 采用的是 CommonJS 模塊規(guī)范,從 Node v13.2.0 版本開始正式支持 ES Modules 特性。直到 v15.3.0 版本 ES Modules 特性才穩(wěn)定下來并與 NPM 生態(tài)相兼容。

Node.js的require函數(shù)中如何添加鉤子

本文將介紹 Node.js 中 require 函數(shù)的工作流程、如何讓 Node.js 直接執(zhí)行 ts 文件及如何正確地劫持 Node.js 的 require 函數(shù),從而實現(xiàn)鉤子的功能。接下來,我們先來介紹 require 函數(shù)。

require 函數(shù)

Node.js 應(yīng)用由模塊組成,每個文件就是一個模塊。對于 CommonJS 模塊規(guī)范來說,我們通過 require 函數(shù)來導(dǎo)入模塊。那么當(dāng)我們使用 require 函數(shù)來導(dǎo)入模塊的時候,該函數(shù)內(nèi)部發(fā)生了什么?這里我們通過調(diào)用堆棧來了解一下 require 的過程:

Node.js的require函數(shù)中如何添加鉤子

由上圖可知,在使用 require 導(dǎo)入模塊時,會調(diào)用 Module 對象的 load 方法來加載模塊,該方法的實現(xiàn)如下所示:

// 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 源碼所對應(yīng)的版本是 v16.13.1

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

  • 步驟一:根據(jù)文件名找出擴展名;

  • 步驟二:通過解析后的擴展名,在 Module._extensions 對象中查找匹配的加載器。

在 Node.js 中內(nèi)置了 3 種不同的加載器,用于加載 node、jsonjs 文件。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 文件的內(nèi)容;

  • 步驟二:使用 module._compile 方法編譯已加載的 js 代碼。

那么了解以上的知識之后,對我們有什么用處呢?其實在了解 require 函數(shù)的工作流程之后,我們就可以擴展 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);
};

在以上代碼中,我們引入了內(nèi)置的 module 模塊,然后利用該模塊的 _extensions 對象來注冊我們的自定義 ts 加載器。

其實,加載器的本質(zhì)就是一個函數(shù),在該函數(shù)內(nèi)部我們利用 esbuild 模塊提供的 transformSync API 來實現(xiàn) ts -> js代碼的轉(zhuǎn)換。當(dāng)完成代碼轉(zhuǎn)換之后,會調(diào)用 module._compile 方法對代碼進行編譯操作。

看到這里相信有的小伙伴,也想到了 Webpack 中對應(yīng)的 loader,想深入學(xué)習(xí)的話,可以閱讀 多圖詳解,一次性搞懂Webpack Loader 這篇文章。

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

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

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

即利用 -r, --require 命令行配置項,我們就可以預(yù)加載指定的模塊。了解完相關(guān)知識之后,我們來測試一下自定義 ts 加載器。首先創(chuàng)建一個 index.ts 文件并輸入以下內(nèi)容:

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

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

然后在命令行輸入以下命令:

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

當(dāng)以上命令成功運行之后,控制臺會輸出以下內(nèi)容:

add(a, b) =  8

很明顯我們自定義的 ts 文件加載器生效了,這種擴展機制還是值得我們學(xué)習(xí)的。另外,需要注意的是在 load 方法中,findLongestRegisteredExtension 函數(shù)會判斷文件的擴展名是否已經(jīng)注冊在 Module._extensions 對象中,若未注冊的話,默認(rèn)會返回 .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 函數(shù)就能正常加載它。比如下面的 a.txt文件:

  module.exports = "hello world";

看到這里相信你已經(jīng)了解 require 函數(shù)是如何加載模塊及如何自定義 Node.js 文件加載器。那么,讓 Node.js 支持加載 ts、pngcss 等其它類型的文件,有更優(yōu)雅、更簡單的方案么?答案是有的,我們可以使用 pirates 這個第三方庫。

pirates 是什么

pirates 這個庫讓我們可以正確地劫持 Node.js 的 require 函數(shù)。利用這個庫,我們就可以很容易擴展 Node.js 加載器的功能。

pirates 的用法

你可以使用 npm 來安裝 pirates:

npm install --save pirates

在成功安裝 pirates 這個庫之后,就可以利用該模塊導(dǎo)出提供的 addHook 函數(shù)來添加鉤子:

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

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

需要注意的是調(diào)用 addHook 之后會返回一個 revert 函數(shù),用于取消對 require 函數(shù)的劫持操作。下面我們來驗證一下 pirates 這個庫是否能正常工作,首先新建一個 index.js 文件并輸入以下內(nèi)容:

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

然后在命令行輸入以下命令:

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

當(dāng)以上命令成功運行之后,控制臺會輸出以下內(nèi)容:

console.log('foo');

觀察以上結(jié)果可知,我們通過 addHook 函數(shù)添加的鉤子生效了。是不是覺得挺神奇的,接下來我們來分析一下 pirates 的工作原理。

pirates 是如何工作的

pirates 底層是利用 Node.js 內(nèi)置 module 模塊提供的擴展機制來實現(xiàn) Hook 功能。前面我們已經(jīng)介紹過了,當(dāng)使用 require 函數(shù)來加載模塊時,Node.js 會根據(jù)文件的后綴名來匹配對應(yīng)的加載器。 其實 pirates 的源碼并不會復(fù)雜,我們來重點分析 addHook 函數(shù)的核心處理邏輯:

// 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) { 
    // ... 
  }
}

為了提高執(zhí)行效率,addHook 函數(shù)提供了 matcherignoreNodeModules 配置項來實現(xiàn)文件過濾操作。在獲取到 exts 擴展名列表之后,就會使用新的加載器來替換已有的加載器。

exts.forEach((ext) => {
    if (typeof ext !== 'string') {
      throw new TypeError(`Invalid Extension: ${ext}`);
    }
    // 獲取已注冊的loader,若未找到,則默認(rèn)使用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) {
			// 這里需要恢復(fù)成原來的_compile函數(shù),否則會出現(xiàn)死循環(huán)
            mod._compile = compile;
			// 在編譯前先執(zhí)行用戶自定義的hook函數(shù)
            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 函數(shù)內(nèi)部是通過替換 mod._compile 方法來實現(xiàn)鉤子的功能。即在調(diào)用原始的 mod._compile 方法進行編譯前,會先調(diào)用 hook(code, filename) 函數(shù)來執(zhí)行用戶自定義的 hook 函數(shù),從而對代碼進行處理。

好的,至此本文的主要內(nèi)容都介紹完了,在實際工作中,如果你想讓 Node.js 直接執(zhí)行 ts 文件,可以利用 ts-node 或 esbuild-register 這兩個庫。其中 esbuild-register 這個庫內(nèi)部就是使用了 pirates 提供的 Hook 機制來實現(xiàn)對應(yīng)的功能。

讀到這里,這篇“Node.js的require函數(shù)中如何添加鉤子”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。

分享題目:Node.js的require函數(shù)中如何添加鉤子
分享地址:http://www.muchs.cn/article34/gcehse.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制網(wǎng)站小程序開發(fā)、網(wǎng)站改版、用戶體驗外貿(mào)網(wǎng)站建設(shè)、服務(wù)器托管

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)