ホームページ  >  記事  >  ウェブフロントエンド  >  Node.js の簡単な分析で、require 関数にフックを追加する方法を詳細に学習します。

Node.js の簡単な分析で、require 関数にフックを追加する方法を詳細に学習します。

青灯夜游
青灯夜游転載
2022-02-09 19:04:542421ブラウズ

Node の require 関数にフックを追加するにはどうすればよいですか? require関数にフックを追加する方法は以下の記事で紹介していますので、ご参考になれば幸いです。

Node.js の簡単な分析で、require 関数にフックを追加する方法を詳細に学習します。

Node.js は、Chrome V8 エンジンに基づく JavaScript ランタイム環境です。初期の Node.js は CommonJS モジュール仕様を採用し、Node v13.2.0 から ES モジュール機能を正式にサポートしました。 ES モジュール機能が安定し、NPM エコシステムと互換性を持つようになったのは、v15.3.0 になってからです。

Node.js の簡単な分析で、require 関数にフックを追加する方法を詳細に学習します。

この記事では、Node.js の require 関数のワークフロー、Node.js で ts ファイルを直接実行する方法、および正しく実行する方法を紹介します。 Node.js の require 関数をハイジャックしてフック関数を実装します。次に、まず require 関数を紹介します。

require function

Node.js アプリケーションはモジュールで構成されており、各ファイルがモジュールです。 CommonJS モジュール仕様の場合、require 関数を通じてモジュールをインポートします。では、require 関数を使用してモジュールをインポートすると、関数内で何が起こるのでしょうか?ここでは、コール スタックを使用して require のプロセスを理解します。

Node.js の簡単な分析で、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 について言及しています。ソース コードに対応するバージョンは

v16.13.1

上記のコードでは、2 つの重要な手順は次のとおりです。

##ステップ 1: ファイルに従って拡張機能名を見つけます;
  • ステップ 2: 解析された拡張機能名を通じて
  • Module._extensions
  • オブジェクト内の一致するローダーを見つけます。
  • Node.js には、
node

jsonjs ファイルをロードするための 3 つの異なるローダーが組み込まれています。 ノード ファイル ローダー

// 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 ローダーのコア処理フローも 2 つのステップに分割できることがわかります。

ステップ 1:
    fs を使用します。 readFileSync
  • js ファイルの内容をロードするメソッド; ステップ 2:
  • 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 の対応するローダーについて考えた友人もいると思います。さらに詳しく知りたい場合は、複数の画像付きの詳細な説明を読んで、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 支持加载 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相关知识,请访问:nodejs 教程

以上がNode.js の簡単な分析で、require 関数にフックを追加する方法を詳細に学習します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。