如何为 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 函数添加钩子?的主要内容,如果未能解决你的问题,请参考以下文章