您好,欢迎访问一九零五行业门户网

Node.js深入学习之浅析require函数中怎么添加钩子

如何为 node 的 require 函数添加钩子?下面本篇文章就来带大家了解一下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 函数。
require 函数node.js 应用由模块组成,每个文件就是一个模块。对于 commonjs 模块规范来说,我们通过 require 函数来导入模块。那么当我们使用 require 函数来导入模块的时候,该函数内部发生了什么?这里我们通过调用堆栈来了解一下 require 的过程:
由上图可知,在使用 require 导入模块时,会调用 module 对象的 load 方法来加载模块,该方法的实现如下所示:
// lib/internal/modules/cjs/loader.jsmodule.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.jsmodule._extensions['.node'] = function(module, filename) { return process.dlopen(module, path.tonamespacedpath(filename));};
json 文件加载器
// lib/internal/modules/cjs/loader.jsmodule._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.jsmodule._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.jsconst 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.tsconst 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.jsmodule.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 是什么pirates 这个库让我们可以正确地劫持 node.js 的 require 函数。利用这个库,我们就可以很容易扩展 node.js 加载器的功能。
pirates 的用法你可以使用 npm 来安装 pirates:
npm install --save pirates
在成功安装 pirates 这个库之后,就可以利用该模块导出提供的 addhook 函数来添加钩子:
// register.jsconst addhook = require("pirates").addhook;const revert = addhook( (code, filename) => code.replace("@@foo", "console.log('foo');"), { exts: [".js"] });
需要注意的是调用 addhook 之后会返回一个 revert 函数,用于取消对 require 函数的劫持操作。下面我们来验证一下 pirates 这个库是否能正常工作,首先新建一个 index.js 文件并输入以下内容:
// index.jsconsole.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.jsexport 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函数中怎么添加钩子的详细内容。
其它类似信息

推荐信息