Node の require 関数にフックを追加するにはどうすればよいですか? require関数にフックを追加する方法は以下の記事で紹介していますので、ご参考になれば幸いです。
Node.js は、Chrome V8 エンジンに基づく JavaScript ランタイム環境です。初期の Node.js は CommonJS モジュール仕様を採用し、Node v13.2.0 から ES モジュール機能を正式にサポートしました。 ES モジュール機能が安定し、NPM エコシステムと互換性を持つようになったのは、v15.3.0 になってからです。
この記事では、Node.js の require
関数のワークフロー、Node.js で ts ファイルを直接実行する方法、および正しく実行する方法を紹介します。 Node.js の require
関数をハイジャックしてフック関数を実装します。次に、まず require
関数を紹介します。
Node.js アプリケーションはモジュールで構成されており、各ファイルがモジュールです。 CommonJS モジュール仕様の場合、require
関数を通じてモジュールをインポートします。では、require
関数を使用してモジュールをインポートすると、関数内で何が起こるのでしょうか?ここでは、コール スタックを使用して require
のプロセスを理解します。
上の図からわかるように、require# を使用するときは、 ## module をインポートするには、
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 について言及しています。ソース コードに対応するバージョンは上記のコードでは、2 つの重要な手順は次のとおりです。 ##ステップ 1: ファイルに従って拡張機能名を見つけます;v16.13.1
、json
、js
ファイルをロードするための 3 つの異なるローダーが組み込まれています。 ノード ファイル ローダー
// lib/internal/modules/cjs/loader.js
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path.toNamespacedPath(filename));
};
// 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;
}
};
// 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 ローダーのコア処理フローも 2 つのステップに分割できることがわかります。
js
ファイルの内容をロードするメソッド;
ステップ 2: 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 の対応するローダーについて考えた友人もいると思います。さらに詳しく知りたい場合は、複数の画像付きの詳細な説明を読んで、Webpack Loader の記事を理解してください。 1回。
アドレス: 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相关知识,请访问:nodejs 教程!
以上がNode.js の簡単な分析で、require 関数にフックを追加する方法を詳細に学習します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。