您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Node.js的require函數中如何添加鉤子”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Node.js的require函數中如何添加鉤子”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
Node.js 是一個基于 Chrome V8 引擎的 JavaScript 運行時環境。早期的 Node.js 采用的是 CommonJS 模塊規范,從 Node v13.2.0 版本開始正式支持 ES Modules 特性。直到 v15.3.0 版本 ES Modules 特性才穩定下來并與 NPM 生態相兼容。
本文將介紹 Node.js 中 require
函數的工作流程、如何讓 Node.js 直接執行 ts 文件及如何正確地劫持 Node.js 的 require
函數,從而實現鉤子的功能。接下來,我們先來介紹 require
函數。
Node.js 應用由模塊組成,每個文件就是一個模塊。對于 CommonJS 模塊規范來說,我們通過 require
函數來導入模塊。那么當我們使用 require
函數來導入模塊的時候,該函數內部發生了什么?這里我們通過調用堆棧來了解一下 require
的過程:
由上圖可知,在使用 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 種不同的加載器,用于加載 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); };
下面我們來分析比較重要的 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 支持加載 ts
、png
或 css
等其它類型的文件,有更優雅、更簡單的方案么?答案是有的,我們可以使用 pirates 這個第三方庫。
pirates 這個庫讓我們可以正確地劫持 Node.js 的 require
函數。利用這個庫,我們就可以很容易擴展 Node.js 加載器的功能。
你可以使用 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 底層是利用 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
函數提供了 matcher
和 ignoreNodeModules
配置項來實現文件過濾操作。在獲取到 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-node 或 esbuild-register 這兩個庫。其中 esbuild-register 這個庫內部就是使用了 pirates 提供的 Hook 機制來實現對應的功能。
讀到這里,這篇“Node.js的require函數中如何添加鉤子”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。