Вступление
Асинхронные хуки - это основной модуль в 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 события, которые мы можем отслеживать с помощью асинхронных хуков. Это включает:
init
- вызывается во время создания асинхронного ресурсаbefore
- вызывается перед вызовом обратного вызова ресурсаafter
- вызывается после вызова обратного вызова ресурсаdestroy
- Вызывается после уничтожения асинхронного ресурса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 дало множество преимуществ и вариантов использования. Некоторые из них включают:
- Лучшая отладка - используя Async Hooks, мы можем улучшить и обогатить трассировку стека асинхронных функций.
- Мощные возможности трассировки, особенно в сочетании с API производительности Node. Кроме того, поскольку API-интерфейс Async Hooks является собственным, накладные расходы на производительность минимальны.
- Обработка контекста веб-запроса - для сбора информации о запросе в течение срока его существования без передачи объекта запроса повсюду. Используя 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. Вы можете прочитать об этом здесь. Кроме того, здесь можно найти код этого руководства.