在Node.js中使用异步挂钩进行请求上下文处理

Using Async Hooks for Request Context Handling in Node.js

介绍

异步挂钩是Node.js中的核心模块,它提供API来跟踪Node应用程序中异步资源的生存期。 异步资源可以被认为是具有关联回调的对象。

示例包括但不限于:Promises,Timeouts,TCPWrap,UDP等。我们可以在此处使用此API跟踪的异步资源的完整列表。

Async Hooks功能于2017年在Node.js版本8中引入,目前仍处于试验阶段。 这意味着仍可能对API的将来版本进行向后不兼容的更改。 话虽如此,它目前不适合生产。

在本文中,我们将深入研究异步挂钩-它们是什么,为什么重要,我们可以在哪里使用它们,以及如何在特定用例(即在Node中请求上下文处理)中利用它们。 js和Express应用程序。

什么是异步挂钩?

如前所述,Async Hooks类是一个核心的Node.js模块,它提供用于跟踪Node.js应用程序中的异步资源的API。 这还包括跟踪由本机节点模块(例如fsnet)创建的资源。

在异步资源的生存期内,有4个事件会触发,我们可以使用Async Hooks进行跟踪。 这些包括:

  • init-在构造异步资源期间调用

  • before-在调用资源的回调之前调用

  • after-在调用资源的回调之后调用

  • destroy-在异步资源被销毁后调用

  • promiseResolve-在调用Promise的resolve()函数时调用。

  • 以下是Node.js文档中概述的Async Hooks API的摘要:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const async_hooks = require('async_hooks');

    const exec_id = async_hooks.executionAsyncId();
    const trigger_id = async_hooks.triggerAsyncId();
    const asyncHook = async_hooks.createHook({
      init: function (asyncId, type, triggerAsyncId, resource) { },
      before: function (asyncId) { },
      after: function (asyncId) { },
      destroy: function (asyncId) { },
      promiseResolve: function (asyncId) { }
    });
    asyncHook.enable();
    asyncHook.disable();

    executionAsyncId()方法返回当前执行上下文的标识符。

    triggerAsyncId()方法返回触发异步资源执行的父资源的标识符。

    createHook()方法创建一个异步钩子实例,将上述事件作为可选回调。

    为了跟踪资源,我们调用了异步钩子实例的enable()方法,该实例是使用createHook()方法创建的。

    我们还可以通过调用disable()函数来禁用跟踪。

    在了解了Async Hooks API的含义之后,我们来看看为什么要使用它。

    何时使用异步挂钩

    将Async Hooks添加到核心API已经获得了许多优势和用例。 其中一些包括:

  • 更好的调试-通过使用异步挂钩,我们可以改善和丰富异步函数的堆栈跟踪。

  • 强大的跟踪功能,尤其是与Node的Performance API结合使用时。 另外,由于Async Hooks API是本机的,因此性能开销最小。

  • Web请求上下文处理-在该请求的生存期内捕获请求的信息,而无需将请求对象传递到任何地方。 使用异步挂钩可以在代码中的任何地方完成,在跟踪服务器中用户的行为时特别有用。

  • 在本文中,我们将研究如何在Express应用程序中使用Async Hooks处理请求ID跟踪。

    使用异步挂钩进行请求上下文处理

    在本节中,我们将说明如何利用异步挂钩在Node.js应用程序中执行简单的请求ID跟踪。

    设置请求上下文处理程序

    我们将首先创建一个目录,应用程序文件将驻留在该目录中,然后移入该目录:

    1
    mkdir async_hooks && cd async_hooks

    接下来,我们需要使用npm和默认设置在此目录中初始化Node.js应用程序:

    1
    npm init -y

    这将在目录的根目录下创建一个package.json文件。

    接下来,我们需要安装Expressuuid软件包作为依赖项。 我们将使用uuid包为每个传入请求生成唯一的ID。

    最后,我们安装esm模块,以便低于v14的Node.js版本可以运行以下示例:

    1
    npm install express uuid esm --save

    接下来,在目录的根目录下创建一个hooks.js文件:

    1
    touch hooks.js

    该文件将包含与async_hooks模块交互的代码。 它导出两个函数:

  • 一个为HTTP请求启用Async Hook,跟踪其给定的请求ID和我们希望保留的任何请求数据的跟踪器。

  • 另一个在给定其异步挂钩ID的情况下返回由挂钩管理的请求数据。

  • 让我们将其放入代码中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    require = require('esm')(module);
    const asyncHooks = require('async_hooks');
    const { v4 } = require('uuid');
    const store = new Map();

    const asyncHook = asyncHooks.createHook({
        init: (asyncId, _, triggerAsyncId) => {
            if (store.has(triggerAsyncId)) {
                store.set(asyncId, store.get(triggerAsyncId))
            }
        },
        destroy: (asyncId) => {
            if (store.has(asyncId)) {
                store.delete(asyncId);
            }
        }
    });

    asyncHook.enable();

    const createRequestContext = (data, requestId = v4()) => {
        const requestInfo = { requestId, data };
        store.set(asyncHooks.executionAsyncId(), requestInfo);
        return requestInfo;
    };

    const getRequestContext = () => {
        return store.get(asyncHooks.executionAsyncId());
    };

    module.exports = { createRequestContext, getRequestContext };

    在这段代码中,我们首先需要esm模块为不具有对实验模块导出的本机支持的Node版本提供向后兼容性。 uuid模块在内部使用此功能。

    接下来,我们还需要async_hooksuuid模块。 从uuid模块中,我们分解v4函数,稍后将使用该函数生成版本4 UUID。

    接下来,我们创建一个存储,它将每个异步资源映射到其请求上下文。 为此,我们利用了一个简单的JavaScript映射。

    接下来,我们调用async_hooks模块的createHook()方法并实现init()destroy()回调。 在我们的init()回调的实现中,我们检查triggerAsyncId是否在商店中存在。

    如果存在,我们将创建asyncId到存储在triggerAsyncId下的请求数据的映射。 实际上,这确保了我们为子异步资源存储相同的请求对象。

    destroy()回调检查存储是否具有资源的asyncId,如果为true,则将其删除。

    要使用挂钩,我们可以通过调用已创建的asyncHook实例的enable()方法来启用它。

    接下来,我们创建2个函数-createRequestContext()getRequestContext,分别用于创建和获取请求上下文。

    createRequestContext()函数接收请求数据和唯一ID作为参数。 然后,它通过两个参数创建一个requestInfo对象,并尝试使用当前执行上下文的异步ID作为键,并使用requestInfo作为值来更新存储。

    另一方面,getRequestContext()函数检查存储中是否包含与当前执行上下文的ID相对应的ID。

    最后,我们使用module.exports()语法导出这两个函数。

    我们已经成功设置了请求上下文处理功能。 让我们继续设置将接收请求的Express服务器。

    设置Express服务器

    设置完上下文后,我们现在将继续创建Express服务器,以便捕获HTTP请求。 为此,请在目录的根目录中创建一个server.js文件,如下所示:

    1
    touch server.js

    我们的服务器将在端口3000上接受HTTP请求。它通过在中间件函数(可访问HTTP请求和响应对象的函数)中调用createRequestContext()来创建一个异步挂钩来跟踪每个请求。 然后,服务器发送一个JSON响应,其中包含Async Hook捕获的数据。

    server.js文件中,输入以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    const express = require('express');
    const ah = require('./hooks');
    const app = express();
    const port = 3000;

    app.use((request, response, next) => {
        const data = { headers: request.headers };
        ah.createRequestContext(data);
        next();
    });

    const requestHandler = (request, response, next) => {
        const reqContext = ah.getRequestContext();
        response.json(reqContext);
        next()
    };

    app.get('/', requestHandler)

    app.listen(port, (err) => {
        if (err) {
            return console.error(err);
        }
        console.log(`server is listening on ${port}`);
    });

    在这段代码中,我们需要expresshooks模块作为依赖项。 然后,我们通过调用express()函数来创建express应用。

    接下来,我们设置一个中间件,该中间件对请求标头进行解构,将其保存到名为data的变量中。 然后,它调用createRequestContext()函数并将data作为参数传递。 这样可以确保使用异步挂钩在请求的整个生命周期中保留请求的标头。

    最后,我们调用next()函数转到中间件管道中的下一个中间件或调用下一个路由处理程序。

    在我们的中间件之后,我们编写了requestHandler()函数来处理服务器根域上的GET请求。 您会注意到,在此函数中,我们可以通过getRequestContext()函数访问请求上下文。 该函数返回一个对象,该对象表示在请求上下文中生成并存储的请求标头和请求ID。

    然后,我们创建一个简单的端点并将请求处理程序附加为回调。

    最后,通过调用应用实例的listen()方法,使服务器侦听端口3000上的连接。

    在运行代码之前,打开目录根目录下的package.json文件,并用以下内容替换脚本的test部分:

    1
    "start":"node server.js"

    完成后,我们可以使用以下命令运行我们的应用程序:

    1
    npm start

    您应该在终端上收到响应,指示该应用程序正在端口3000上运行,如下所示:

    1
    2
    3
    4
    5
    > <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3554464c5b56185d5a5a5e46185150585a75041b051b05">[emailprotected]</a> start /Users/allanmogusu/StackAbuse/async-hooks-demo
    > node server.js

    (node:88410) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time
    server is listening on 3000

    在我们的应用程序运行的情况下,打开一个单独的终端实例并运行以下curl命令以测试我们的默认路由:

    1
    curl http://localhost:3000

    curl命令向我们的默认路由发出GET请求。 您应该得到类似于以下的响应:

    1
    2
    $ curl http://localhost:3000
    {"requestId":"3aad88a6-07bb-41e0-ab5a-fa9d5c0269a7","data":{"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"}}}%

    请注意,将返回生成的requestId和我们的请求标头。 重复该命令应生成一个新的请求ID,因为我们将发出一个新请求:

    1
    2
    $ curl http://localhost:3000
    {"requestId":"38da84792-e782-47dc-92b4-691f4285b172","data":{"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"}}}%

    响应包含我们为请求生成的ID和我们在中间件函数中捕获的标头。 借助Async Hooks,我们可以轻松地将数据从一个中间件传递到另一个中间件,以进行相同的请求。

    结论

    异步挂钩提供了一个API,用于跟踪Node.js应用程序中异步资源的生存期。

    在本文中,我们简要介绍了Async Hooks API,其提供的功能以及如何利用它。 我们专门介绍了一个基本示例,说明如何使用异步挂钩来有效,干净地进行Web请求上下文处理和跟踪。

    但是从Node.js版本14开始,Async Hooks API附带了异步本地存储,该API使Node.js中的请求上下文处理更加容易。 你可以在这里读更多关于它的内容。 另外,可以在此处访问本教程的代码。