Использование асинхронных хуков для обработки контекста запроса в Node.js

Введение Асинхронные хуки [https://nodejs.org/api/async_hooks.html#async_hooks_async_hooks] - это основной модуль в Node.js, который предоставляет API для отслеживания времени жизни асинхронных ресурсов в приложении Node. Асинхронный ресурс можно рассматривать как объект, с которым связан обратный вызов. Примеры включают, но не ограничиваются: обещания, таймауты, TCPWrap, UDP и т. Д. Полный список асинхронных ресурсов, которые мы можем отслеживать с помощью этого API, можно найти здесь. [https://nodejs.org/api/

Вступление

Асинхронные хуки - это основной модуль в Node.js, который предоставляет API для отслеживания времени жизни асинхронных ресурсов в приложении Node. Асинхронный ресурс можно рассматривать как объект, с которым связан обратный вызов.

Примеры включают, но не ограничиваются: обещания, таймауты, TCPWrap, UDP и т. Д. Полный список асинхронных ресурсов, которые мы можем отслеживать с помощью этого API, можно найти здесь.

Функция Async Hooks была представлена в 2017 году в Node.js версии 8 и все еще является экспериментальной. Это означает, что обратно несовместимые изменения могут быть внесены в будущие выпуски API. При этом в настоящее время он не считается пригодным для производства.

В этой статье мы более подробно рассмотрим асинхронные хуки - что они собой представляют, почему они важны, где мы можем их использовать и как мы можем использовать их для конкретного варианта использования, то есть обработки контекста запроса в Node. js и экспресс-приложение.

Что такое асинхронные хуки?

Как указывалось ранее, класс Async Hooks - это базовый модуль Node.js, который предоставляет API для отслеживания асинхронных ресурсов в вашем приложении Node.js. Это также включает отслеживание ресурсов, созданных собственными модулями Node, такими как fs и net .

В течение времени существования асинхронного ресурса срабатывают 4 события, которые мы можем отслеживать с помощью асинхронных хуков. Это включает:

  1. init - вызывается во время создания асинхронного ресурса
  2. before - вызывается перед вызовом обратного вызова ресурса
  3. after - вызывается после вызова обратного вызова ресурса
  4. destroy - Вызывается после уничтожения асинхронного ресурса
  5. promiseResolve - Вызывается , когда resolve() функция вызывается Promise.

Ниже приведен обобщенный фрагмент API Async Hooks из обзора в документации Node.js :

 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() нашего экземпляра async hook, который мы создаем с помощью createHook() .

Мы также можем отключить отслеживание, вызвав disable() .

Увидев, что влечет за собой API Async Hooks, давайте разберемся, почему мы должны его использовать.

Когда использовать асинхронные хуки

Добавление Async Hooks к основному API дало множество преимуществ и вариантов использования. Некоторые из них включают:

  1. Лучшая отладка - используя Async Hooks, мы можем улучшить и обогатить трассировку стека асинхронных функций.
  2. Мощные возможности трассировки, особенно в сочетании с API производительности Node. Кроме того, поскольку API-интерфейс Async Hooks является собственным, накладные расходы на производительность минимальны.
  3. Обработка контекста веб-запроса - для сбора информации о запросе в течение срока его существования без передачи объекта запроса повсюду. Используя Async Hooks, это можно сделать в любом месте кода и может быть особенно полезно при отслеживании поведения пользователей на сервере.

В этой статье мы рассмотрим, как обрабатывать отслеживание идентификатора запроса с помощью асинхронных хуков в приложении Express.

Использование асинхронных хуков для обработки контекста запроса

В этом разделе мы проиллюстрируем, как мы можем использовать Async Hooks для выполнения простой трассировки идентификатора запроса в приложении Node.js.

Настройка обработчиков контекста запроса

Мы начнем с создания каталога, в котором будут находиться файлы нашего приложения, а затем перейдем в него:

 mkdir async_hooks && cd async_hooks 

Затем нам нужно инициализировать наше приложение Node.js в этом каталоге с npm и настройками по умолчанию:

 npm init -y 

Это создает package.json в корне каталога.

Затем нам нужно установить Express и uuid качестве зависимостей. Мы будем использовать uuid для генерации уникального идентификатора для каждого входящего запроса.

Наконец, мы устанавливаем esm чтобы версии Node.js ниже v14 могли запускать этот пример:

 npm install express uuid esm --save 

Затем создайте hooks.js в корне каталога:

 touch hooks.js 

Этот файл будет содержать код, который взаимодействует с модулем async_hooks Он экспортирует две функции:

  • Тот, который включает Async Hook для HTTP-запроса, отслеживая его заданный идентификатор запроса и любые данные запроса, которые мы хотели бы сохранить.
  • Другой возвращает данные запроса, управляемые ловушкой, с учетом ее идентификатора Async Hook ID.

Поместим это в код:

 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_hooks и uuid Из uuid мы деструктурируем v4 , которую мы будем использовать позже для генерации UUID версии 4.

Затем мы создаем хранилище, которое будет сопоставлять каждый асинхронный ресурс с его контекстом запроса. Для этого мы используем простую карту JavaScript.

Затем мы вызываем метод createHook() async_hooks и реализуем обратные вызовы init() и destroy() В реализации нашего init() мы проверяем, присутствует ли triggerAsyncId в магазине.

Если он существует, мы создаем сопоставление asyncId с данными запроса, хранящимися в triggerAsyncId . Это фактически гарантирует, что мы сохраним один и тот же объект запроса для дочерних асинхронных ресурсов.

destroy() проверяет, имеет ли хранилище asyncId ресурса, и удаляет его, если он истинен.

Чтобы использовать наш хук, мы включаем его, вызывая метод enable() созданного asyncHook экземпляра asyncHook.

Затем мы создаем 2 функции - createRequestContext() и getRequestContext которые мы используем для создания и получения контекста нашего запроса соответственно.

Функция createRequestContext() получает данные запроса и уникальный идентификатор в качестве аргументов. Затем он создает requestInfo из обоих аргументов и пытается обновить хранилище, используя асинхронный идентификатор текущего контекста выполнения в качестве ключа и requestInfo в качестве значения.

Функция getRequestContext() , с другой стороны, проверяет, содержит ли хранилище идентификатор, соответствующий идентификатору текущего контекста выполнения.

Наконец, мы экспортируем обе функции, используя синтаксис module.exports()

Мы успешно настроили нашу функцию обработки контекста запроса. Приступим к настройке нашего Express сервера, который будет получать запросы.

Настройка экспресс-сервера

Настроив наш контекст, мы приступим к созданию нашего Express сервера, чтобы мы могли захватывать HTTP-запросы. Для этого создайте server.js в корне каталога следующим образом:

 touch server.js 

Наш сервер будет принимать HTTP-запрос на порт 3000. Он создает Async Hook для отслеживания каждого запроса, вызывая createRequestContext() в функции промежуточного программного обеспечения - функции, которая имеет доступ к объектам HTTP-запроса и ответа. Затем сервер отправляет ответ JSON с данными, полученными с помощью Async Hook.

Внутри server.js введите следующий код:

 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}`); 
 }); 

В этом фрагменте кода нам требуются express модули и hooks качестве зависимостей. Затем мы создаем Express , вызывая функцию express()

Затем мы настраиваем промежуточное программное обеспечение, которое разрушает заголовки запросов, сохраняя их в переменной с именем data . Затем он вызывает функцию createRequestContext() data в качестве аргумента. Это гарантирует, что заголовки запроса будут сохранены на протяжении всего жизненного цикла запроса с помощью Async Hook.

Наконец, мы вызываем next() чтобы перейти к следующему промежуточному программному обеспечению в нашем конвейере промежуточного программного обеспечения или вызвать следующий обработчик маршрута.

После нашего промежуточного программного обеспечения мы пишем requestHandler() которая обрабатывает GET в корневом домене сервера. Вы заметите, что в этой функции мы можем получить доступ к нашему контексту запроса через getRequestContext() . Эта функция возвращает объект, представляющий заголовки запроса и идентификатор запроса, сгенерированные и сохраненные в контексте запроса.

Затем мы создаем простую конечную точку и присоединяем наш обработчик запросов в качестве обратного вызова.

Наконец, мы заставляем наш сервер прослушивать соединения на порту 3000, вызывая метод listen() нашего экземпляра приложения.

Перед запуском кода откройте package.json в корне каталога и замените test раздел скрипта следующим:

 "start": "node server.js" 

После этого мы можем запустить наше приложение с помощью следующей команды:

 npm start 

Вы должны получить ответ на своем терминале, указывающий, что приложение работает на порту 3000, как показано:

 > [email protected] 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 чтобы проверить наш маршрут по умолчанию:

 curl http://localhost:3000 

Эта команда curl GET на наш маршрут по умолчанию. Вы должны получить примерно такой ответ:

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

Обратите внимание, что сгенерированный requestId и наши заголовки запроса возвращаются. Повторение команды должно сгенерировать новый идентификатор запроса, поскольку мы будем делать новый запрос:

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

Ответ содержит идентификатор, который мы сгенерировали для запроса, и заголовки, которые мы захватили в функции промежуточного программного обеспечения. С помощью Async Hooks мы могли легко передавать данные от одного промежуточного программного обеспечения к другому для одного и того же запроса.

Заключение

Async Hooks предоставляет API для отслеживания времени жизни асинхронных ресурсов в приложении Node.js.

В этой статье мы кратко рассмотрели API Async Hooks, предоставляемые им функции и способы их использования. Мы специально рассмотрели базовый пример того, как мы можем использовать Async Hooks для эффективной и чистой обработки контекста веб-запроса и его трассировки.

Однако, начиная с Node.js версии 14, Async Hooks API поставляется с асинхронным локальным хранилищем, API, который упрощает обработку контекста запроса в Node.js. Вы можете прочитать об этом здесь. Кроме того, здесь можно найти код этого руководства.

comments powered by Disqus