如何为 Node.js 的 require 函数添加钩子?

Posted 全栈修仙之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何为 Node.js 的 require 函数添加钩子?相关的知识,希望对你有一定的参考价值。

Node.js 是一个基于 Chrome V8 引擎的 javascript 运行时环境。早期的 Node.js 采用的是 CommonJS 模块规范,从 Node v13.2.0 版本开始正式支持 ES Modules 特性。直到 v15.3.0 版本 ES Modules 特性才稳定下来并与 NPM 生态相兼容。

(图片来源:https://nodejs.org/api/esm.html

本文将介绍 Node.js 中 require 函数的工作流程、如何让 Node.js 直接执行 ts 文件及如何正确地劫持 Node.js 的 require 函数,从而实现钩子的功能。接下来,我们先来介绍 require 函数。

函数来导入模块。那么当我们使用 require 函数来导入模块的时候,该函数内部发生了什么?这里我们通过调用堆栈来了解一下 require 的过程:

由上图可知,在使用 require 导入模块时,会调用 Module 对象的 load 方法来加载模块,该方法的实现如下所示:

对象中查找匹配的加载器。
加载器的核心处理流程,也可以分为两个步骤:

  • 步骤一:使用 fs.readFileSync 方法加载 js 文件的内容;
  • 步骤二:使用 module._compile 方法编译已加载的 js 代码。
  • 模块,然后利用该模块的 _extensions 对象来注册我们的自定义 ts 加载器。

    其实,加载器的本质就是一个函数,在该函数内部我们利用 esbuild 模块提供的 transformSync API 来实现 ts -> js 代码的转换。当完成代码转换之后,会调用 module._compile 方法对代码进行编译操作。

    看到这里相信有的小伙伴,也想到了 Webpack 中对应的 loader,想深入学习的话,可以阅读 多图详解,一次性搞懂Webpack Loader 这篇文章。

    篇幅有限,具体的编译过程,我们就不展开介绍了。下面我们来看一下如何让自定义的 ts 加载器生效。要让 Node.js 能够执行 ts 代码,我们就需要在执行 ts 代码前,先完成自定义 ts 加载器的注册操作。庆幸的是,Node.js 为我们提供了模块的预加载机制:

    命令行配置项,我们就可以预加载指定的模块。了解完相关知识之后,我们来测试一下自定义 ts 加载器。

    首先创建一个 index.ts 文件并输入以下内容:

    方法中,findLongestRegisteredExtension 函数会判断文件的扩展名是否已经注册在 Module._extensions 对象中,若未注册的话,默认会返回 .js 字符串。

    代码,require 函数就能正常加载它。比如下面的 a.txt 文件:

    函数是如何加载模块及如何自定义 Node.js 文件加载器。那么让 Node.js 支持加载 tspngcss 等其它类型的文件,有更优雅、更简单的方案么?答案是有的,我们可以使用 pirates 这个第三方库。

    函数来添加钩子:

    之后会返回一个 revert 函数,用于取消对 require 函数的劫持操作。下面我们来验证一下 pirates 这个库是否能正常工作,首先新建一个 index.js 文件并输入以下内容:

    函数添加的钩子生效了。是不是觉得挺神奇的,接下来我们来分析一下 pirates 的工作原理。

    模块提供的扩展机制来实现 Hook 功能。前面我们已经介绍过了,当使用 require 函数来加载模块时,Node.js 会根据文件的后缀名来匹配对应的加载器。

    其实 pirates 的源码并不会复杂,我们来重点分析 addHook 函数的核心处理逻辑:

    函数提供了 matcherignoreNodeModules 配置项来实现文件过滤操作。在获取到 exts 扩展名列表之后,就会使用新的加载器来替换已有的加载器。

    函数内部是通过替换 mod._compile 方法来实现钩子的功能。即在调用原始的 mod._compile 方法进行编译前,会先调用 hook(code, filename) 函数来执行用户自定义的 hook 函数,从而对代码进行处理。

    好的,至此本文的主要内容都介绍完了,在实际工作中,如果你想让 Node.js 直接执行 ts 文件,可以利用 ts-node 或 esbuild-register 这两个库。其中 esbuild-register 这个库内部就是使用了 pirates 提供的 Hook 机制来实现对应的功能。

    如何为 Node.js 编写异步函数

    【中文标题】如何为 Node.js 编写异步函数【英文标题】:How to write asynchronous functions for Node.js 【发布时间】:2011-10-17 10:43:01 【问题描述】:

    我试图研究应该如何编写异步函数。在翻阅了大量文档之后,我仍然不清楚。

    如何为 Node 编写异步函数?我应该如何正确实现错误事件处理?

    问我问题的另一种方式是:我应该如何解释以下函数?

    var async_function = function(val, callback)
        process.nextTick(function()
            callback(val);
        );
    ;
    

    另外,我发现 this question on SO(“如何在 node.js 中创建非阻塞异步函数?”)很有趣。我觉得还没有回答。

    【问题讨论】:

    这就是我问的原因。我看不出这些功能有何不同。 我建议您在自己喜欢的浏览器中查看setTimeoutsetInterval 并与它们一起玩。或者 ajax 回调(可能是最接近节点体验的东西),或者你熟悉的事件监听器,比如点击和加载事件。异步模型在浏览器中已经存在,在node中完全一样。 @davin - 那时我猜我还没有完全理解异步模型。 @Kriem,我昨天回答了一些可能有帮助的问题:***.com/questions/6883648/… 这不是你问题的答案,但它是主题。尝试阅读那里的问题和答案,并尝试使用代码来尝试了解发生了什么。 @Raynos “异步函数”的定义是什么? 【参考方案1】:

    您似乎将异步 IO 与异步函数混淆了。 node.js 使用异步非阻塞 IO,因为非阻塞 IO 更好。了解它的最好方法是去看 ryan dahl 的一些视频。

    如何为 Node 编写异步函数?

    只写普通函数,唯一的区别是它们不是立即执行而是作为回调传递。

    我应该如何正确实现错误事件处理

    通常,API 会为您提供一个回调,并将错误作为第一个参数。例如

    database.query('something', function(err, result) 
      if (err) handle(err);
      doSomething(result);
    );
    

    是一种常见的模式。

    另一个常见的模式是on('error')。例如

    process.on('uncaughtException', function (err) 
      console.log('Caught exception: ' + err);
    );
    

    编辑:

    var async_function = function(val, callback)
        process.nextTick(function()
            callback(val);
        );
    ;
    

    上述函数调用时

    async_function(42, function(val) 
      console.log(val)
    );
    console.log(43);
    

    将异步打印42 到控制台。特别是 process.nextTick 在当前事件循环调用堆栈为空后触发。在 async_functionconsole.log(43) 运行后,该调用堆栈为空。所以我们打印 43,然后打印 42。

    您可能应该阅读一下事件循环。

    【讨论】:

    我看过 Dahl 的视频,但恐怕我并没有掌握这件事。 :( @Kriem 查看更新的答案并阅读about the event loop 感谢您的见解。我现在更加意识到我缺乏知识。 :) 顺便说一句,您的最后一个示例有所帮助。 我认为您关于异步 IO 的说法“更好”太笼统了。从这个意义上说是的,但总体而言可能并非如此。 在您的第一个代码示例中,您检查了 err 参数,但之后没有返回。如果出现错误,代码将继续运行,并可能导致您的应用程序出现严重问题。【参考方案2】:

    仅仅通过回调是不够的。 例如,您必须使用 settimer 来使函数异步。

    示例: 不是异步函数:

    function a() 
      var a = 0;    
      for(i=0; i<10000000; i++) 
        a++;
      ;
      b();
    ;
    
    function b() 
      var a = 0;    
      for(i=0; i<10000000; i++) 
        a++;
      ;    
      c();
    ;
    
    function c() 
      for(i=0; i<10000000; i++) 
      ;
      console.log("async finished!");
    ;
    
    a();
    console.log("This should be good");
    

    如果你要运行上面的例子,这应该很好,必须等到这些函数完成才能工作。

    伪多线程(异步)函数:

    function a() 
      setTimeout ( function() 
        var a = 0;  
        for(i=0; i<10000000; i++) 
          a++;
        ;
        b();
      , 0);
    ;
    
    function b() 
      setTimeout ( function() 
        var a = 0;  
        for(i=0; i<10000000; i++) 
          a++;
        ;  
        c();
      , 0);
    ;
    
    function c() 
      setTimeout ( function() 
        for(i=0; i<10000000; i++) 
        ;
        console.log("async finished!");
      , 0);
    ;
    
    a();
    console.log("This should be good");
    

    这将是真正的异步。 这应该会在异步完成之前写好。

    【讨论】:

    【参考方案3】:

    你应该看这个:Node Tuts episode 19 - Asynchronous Iteration Patterns

    它应该回答你的问题。

    【讨论】:

    【参考方案4】:

    如果你知道一个函数返回一个 promise,我建议使用 JavaScript 中新的 async/await 特性。它使语法看起来同步但异步工作。当您将 async 关键字添加到函数时,它允许您在该范围内 await 承诺:

    async function ace() 
      var r = await new Promise((resolve, reject) => 
        resolve(true)
      );
    
      console.log(r); // true
    
    

    如果一个函数没有返回一个promise,我建议将它包装在一个你定义的新promise中,然后解析你想要的数据:

    function ajax_call(url, method) 
      return new Promise((resolve, reject) => 
        fetch(url,  method )
        .then(resp => resp.json())
        .then(json =>  resolve(json); )
      );
    
    
    async function your_function() 
      var json = await ajax_call('www.api-example.com/some_data', 'GET');
      console.log(json); //  status: 200, data: ... 
    
    

    底线:利用 Promises 的力量。

    【讨论】:

    这里要记住的是,promise 的主体仍然是同步执行的。【参考方案5】:

    试试这个,它适用于节点和浏览器。

    isNode = (typeof exports !== 'undefined') &&
    (typeof module !== 'undefined') &&
    (typeof module.exports !== 'undefined') &&
    (typeof navigator === 'undefined' || typeof navigator.appName === 'undefined') ? true : false,
    asyncIt = (isNode ? function (func) 
      process.nextTick(function () 
        func();
      );
     : function (func) 
      setTimeout(func, 5);
    );
    

    【讨论】:

    4 票反对,甚至没有一条建设性评论.. :\ @Omer 这就是 SO 上的生活。 @NorbertoBezi 也许代码对您来说是不言自明的,但对于发布答案的人来说却不是。这就是为什么在投反对票时解释总是一个好习惯的原因。【参考方案6】:

    我已经为 node.js 中的此类任务处理了太多时间。我主要是前端的人。

    我觉得这点很重要,因为所有节点方法都是异步处理回调的,把它转换成 Promise 处理比较好。

    我只是想展示一个可能的结果,更精简和可读。将 ECMA-6 与 async 一起使用,您可以这样编写。

     async function getNameFiles (dirname) 
      return new Promise((resolve, reject) => 
        fs.readdir(dirname, (err, filenames) => 
          err !== (undefined || null) ? reject(err) : resolve(filenames)
        )
      )
    
    

    (undefined || null) 用于 repl(读取事件打印循环)场景, 使用 undefined 也可以。

    【讨论】:

    以上是关于如何为 Node.js 的 require 函数添加钩子?的主要内容,如果未能解决你的问题,请参考以下文章

    如何为 Node.js 编写异步函数

    如何为特定令牌、颤振、firebase 消息传递和 node.js 发送通知

    如何为特定版本的 node.js 安装 d.ts?

    如何为 Node JS 设置 REST API

    如何为 80 端口上的网站制作 Node.js 多租户?

    如何为 Azure 上的 Node.js Api 应用启用 BLOB 日志记录?