新聞中心
(圖片來(lái)源:https://nodejs.org/api/esm.html)

創(chuàng)新互聯(lián)建站2013年至今,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元東鄉(xiāng)族做網(wǎng)站,已為上家服務(wù),為東鄉(xiāng)族各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:13518219792
本文將介紹 Node.js 中 require 函數(shù)的工作流程、如何讓 Node.js 直接執(zhí)行 ts 文件及如何正確地劫持 Node.js 的 require 函數(shù),從而實(shí)現(xiàn)鉤子的功能。接下來(lái),我們先來(lái)介紹 require 函數(shù)。
require 函數(shù)
Node.js 應(yīng)用由模塊組成,每個(gè)文件就是一個(gè)模塊。對(duì)于 CommonJS 模塊規(guī)范來(lái)說(shuō),我們通過(guò) require 函數(shù)來(lái)導(dǎo)入模塊。那么當(dāng)我們使用 require 函數(shù)來(lái)導(dǎo)入模塊的時(shí)候,該函數(shù)內(nèi)部發(fā)生了什么?這里我們通過(guò)調(diào)用堆棧來(lái)了解一下 require的過(guò)程:
由上圖可知,在使用 require 導(dǎo)入模塊時(shí),會(huì)調(diào)用 Module 對(duì)象的 load 方法來(lái)加載模塊,該方法的實(shí)現(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 源碼所對(duì)應(yīng)的版本是 v16.13.1
在以上代碼中,重要的兩個(gè)步驟是:
- 步驟一:根據(jù)文件名找出擴(kuò)展名;
- 步驟二:通過(guò)解析后的擴(kuò)展名,在 Module._extensions 對(duì)象中查找匹配的加載器。
在 Node.js 中內(nèi)置了 3 種不同的加載器,用于加載 node、json 和 js 文件。
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);
};
下面我們來(lái)分析比較重要的 js 文件加載器。通過(guò)觀察以上代碼,我們可知 js 加載器的核心處理流程,也可以分為兩個(gè)步驟:
- 步驟一:使用 fs.readFileSync 方法加載js 文件的內(nèi)容;
- 步驟二:使用 module._compile 方法編譯已加載的 js 代碼。
那么了解以上的知識(shí)之后,對(duì)我們有什么用處呢?其實(shí)在了解 require 函數(shù)的工作流程之后,我們就可以擴(kuò)展 Node.js 的加載器。比如讓 Node.js 能夠運(yùn)行 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 對(duì)象來(lái)注冊(cè)我們的自定義 ts 加載器。
其實(shí),加載器的本質(zhì)就是一個(gè)函數(shù),在該函數(shù)內(nèi)部我們利用 esbuild 模塊提供的 transformSyncAPI 來(lái)實(shí)現(xiàn) ts -> js 代碼的轉(zhuǎn)換。當(dāng)完成代碼轉(zhuǎn)換之后,會(huì)調(diào)用 module._compile 方法對(duì)代碼進(jìn)行編譯操作。
看到這里相信有的小伙伴,也想到了 Webpack 中對(duì)應(yīng)的 loader,想深入學(xué)習(xí)的話,可以閱讀 多圖詳解,一次性搞懂Webpack Loader 這篇文章。
篇幅有限,具體的編譯過(guò)程,我們就不展開(kāi)介紹了。下面我們來(lái)看一下如何讓自定義的 ts 加載器生效。要讓 Node.js 能夠執(zhí)行 ts 代碼,我們就需要在執(zhí)行 ts 代碼前,先完成自定義 ts 加載器的注冊(cè)操作。慶幸的是,Node.js 為我們提供了模塊的預(yù)加載機(jī)制:
$ node --help | grep preload
-r, --require=... module to preload (option can be repeated)
即利用 -r, --require 命令行配置項(xiàng),我們就可以預(yù)加載指定的模塊。了解完相關(guān)知識(shí)之后,我們來(lái)測(cè)試一下自定義 ts 加載器。
首先創(chuàng)建一個(gè) 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)以上命令成功運(yùn)行之后,控制臺(tái)會(huì)輸出以下內(nèi)容:
add(a, b) = 8
很明顯我們自定義的 ts 文件加載器生效了,這種擴(kuò)展機(jī)制還是值得我們學(xué)習(xí)的。另外,需要注意的是在 load 方法中,findLongestRegisteredExtension 函數(shù)會(huì)判斷文件的擴(kuò)展名是否已經(jīng)注冊(cè)在 Module._extensions 對(duì)象中,若未注冊(cè)的話,默認(rèn)會(huì)返回 .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、png 或 css 等其它類型的文件,有更優(yōu)雅、更簡(jiǎn)單的方案么?答案是有的,我們可以使用 pirates 這個(gè)第三方庫(kù)。
module.exports = "hello world";
pirates 是什么
pirates 這個(gè)庫(kù)讓我們可以正確地劫持 Node.js 的 require 函數(shù)。利用這個(gè)庫(kù),我們就可以很容易擴(kuò)展 Node.js 加載器的功能。
pirates 的用法
你可以使用 npm 來(lái)安裝 pirates:
npm install --save pirates
在成功安裝 pirates 這個(gè)庫(kù)之后,就可以利用該模塊導(dǎo)出提供的 addHook 函數(shù)來(lái)添加鉤子:
// register.js
const addHook = require("pirates").addHook;
const revert = addHook(
(code, filename) => code.replace("@@foo", "console.log('foo');"),
{ exts: [".js"] }
);
需要注意的是調(diào)用 addHook 之后會(huì)返回一個(gè) revert 函數(shù),用于取消對(duì) require 函數(shù)的劫持操作。下面我們來(lái)驗(yàn)證一下 pirates 這個(gè)庫(kù)是否能正常工作,首先新建一個(gè) index.js 文件并輸入以下內(nèi)容:
// index.js
console.log("@@foo")
然后在命令行輸入以下命令:
$ node -r ./register.js index.js
當(dāng)以上命令成功運(yùn)行之后,控制臺(tái)會(huì)輸出以下內(nèi)容:
console.log('foo');觀察以上結(jié)果可知,我們通過(guò) addHook 函數(shù)添加的鉤子生效了。是不是覺(jué)得挺神奇的,接下來(lái)我們來(lái)分析一下 pirates 的工作原理。
pirates 是如何工作的
pirates 底層是利用 Node.js 內(nèi)置 module 模塊提供的擴(kuò)展機(jī)制來(lái)實(shí)現(xiàn) Hook 功能。前面我們已經(jīng)介紹過(guò)了,當(dāng)使用 require 函數(shù)來(lái)加載模塊時(shí),Node.js 會(huì)根據(jù)文件的后綴名來(lái)匹配對(duì)應(yīng)的加載器。
其實(shí) pirates 的源碼并不會(huì)復(fù)雜,我們來(lái)重點(diǎn)分析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ù)提供了 matcher 和 ignoreNodeModules 配置項(xiàng)來(lái)實(shí)現(xiàn)文件過(guò)濾操作。在獲取到 exts 擴(kuò)展名列表之后,就會(huì)使用新的加載器來(lái)替換已有的加載器。
exts.forEach((ext) => {
if (typeof ext !== 'string') {
throw new TypeError(`Invalid Extension: ${ext}`);
}
// 獲取已注冊(cè)的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ù)成原來(lái)的_compile函數(shù),否則會(huì)出現(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)部是通過(guò)替換 mod._compile 方法來(lái)實(shí)現(xiàn)鉤子的功能。即在調(diào)用原始的 mod._compile 方法進(jìn)行編譯前,會(huì)先調(diào)用 hook(code, filename) 函數(shù)來(lái)執(zhí)行用戶自定義的 hook 函數(shù),從而對(duì)代碼進(jìn)行處理。
好的,至此本文的主要內(nèi)容都介紹完了,在實(shí)際工作中,如果你想讓 Node.js 直接執(zhí)行 ts 文件,可以利用 ts-node 或 esbuild-register 這兩個(gè)庫(kù)。其中 esbuild-register 這個(gè)庫(kù)內(nèi)部就是使用了 pirates 提供的 Hook 機(jī)制來(lái)實(shí)現(xiàn)對(duì)應(yīng)的功能。
網(wǎng)頁(yè)標(biāo)題:如何為Node.js的Require函數(shù)添加鉤子?
文章鏈接:http://www.dlmjj.cn/article/dhpdpji.html


咨詢
建站咨詢
