Middleware
Функции-слушатели, которые передаются в bot
, bot
и им подобные, называются middleware. Хотя неправильно говорить, что они слушают обновления, называть их “слушателями” — это упрощение.
В этом разделе объясняется, что такое middleware, и на примере grammY показано, как его можно использовать. Если вы ищете конкретную документацию о том, что делает реализацию middleware в grammY особенной, посмотрите Возможности Middleware в расширенном разделе документации.
Стек Middleware
Предположим, вы пишете бота следующего вида:
const bot = new Bot("");
bot.use(session());
bot.command("start", (ctx) => ctx.reply("Запущен!"));
bot.command("help", (ctx) => ctx.reply("Текст помощи"));
bot.on(":text", (ctx) => ctx.reply("Текст!")); // (*)
bot.on(":photo", (ctx) => ctx.reply("Фото!"));
bot.start();
2
3
4
5
6
7
8
9
10
11
При поступлении обновления с обычным текстовым сообщением будут выполнены следующие действия:
- Вы отправляете боту сообщение `“Привет!”.
- Middleware получает обновление и выполняет свои session действия.
- Обновление будет проверено на наличие команды
/start
, которая не содержится - Обновление будет проверено на наличие команды
/help
, которая не содержится - Обновление будет проверено на наличие текста в сообщении (или сообщении канала), которое успешно.
- Будет вызван middleware по адресу
(*)
, который обработает обновление, ответив `“Текст!”.
Обновление не проверяется на наличие фотоконтента, потому что middleware по адресу (*)
уже обработало обновление.
Итак, как это работает? Давайте выясним.
Мы можем просмотреть тип Middleware
в документации grammY здесь:
// Для краткости опустите некоторые параметры типа.
type Middleware = MiddlewareFn | MiddlewareObj;
2
Ага! Middleware может быть функцией или объектом. Мы использовали только функции ((ctx)
), поэтому пока проигнорируем объекты middleware и углубимся в тип Middleware
(ссылка):
// Снова опущены параметры типа.
type MiddlewareFn = (ctx: Context, next: NextFunction) => MaybePromise<unknown>;
// с
type NextFunction = () => Promise<void>;
2
3
4
Так, middleware принимает два параметра! До сих пор мы использовали только один, объект контекста ctx
. Мы уже знаем, что такое ctx
, но мы также видим функцию с именем next
. Чтобы понять, что такое next
, нам нужно посмотреть на все middleware, которое вы устанавливаете на объект бота в целом.
Вы можете рассматривать все установленные функции middleware как несколько слоев, которые накладываются друг на друга. Первое middleware (session
в нашем примере) является самым верхним слоем, поэтому оно первым получает каждое обновление. Затем он может решить, хочет ли он обработать обновление или передать его следующему слою (обработчику команды /start
). Функция next
может использоваться для вызова последующего middleware, часто называемого нижележащий middleware. Это также означает, что если вы не вызовете функцию next
в своем middleware, то нижележащие уровни middleware не будут вызваны.
Этот стек функций является стеком middleware.
(ctx, next) => ... |
(ctx, next) => ... |—————вышележащий middleware X
(ctx, next) => ... |
(ctx, next) => ... <— middleware X. Вызовите `next` чтобы пропустить обновления ниже
(ctx, next) => ... |
(ctx, next) => ... |—————нижележащий middleware X
(ctx, next) => ... |
Вспомнив наш предыдущий пример, мы теперь знаем, почему bot
даже не был проверен: middleware в bot
уже обработал обновление, и оно не вызывало next
. На самом деле, он даже не указал next
в качестве параметра. Он просто проигнорировала next
, а значит, не передал обновление.
Давайте попробуем что-нибудь еще с нашими новыми знаниями!
const bot = new Bot("");
bot.on(":text", (ctx) => ctx.reply("Текст!"));
bot.command("start", (ctx) => ctx.reply("Команда!"));
bot.start();
2
3
4
5
6
Если вы запустите вышеупомянутого бота и отправите /start
, вы никогда не увидите ответ, говорящий Команда!
. Давайте проверим, что происходит:
- Вы посылаете боту команду
"
./start" - Middleware
":
получает обновление и проверяет его на наличие текста, что удается, поскольку команды — это текстовые сообщения. Обновление немедленно обрабатывается первым middleware, и ваш бот отвечает “Текст!”.text"
Сообщение даже не проверяется на наличие в нем команды /start
! Порядок регистрации middleware имеет значение, потому что он определяет порядок слоев в стеке middleware. Проблему можно решить, изменив порядок строк 3 и 4. Если бы вы вызвали next
в строке 3, было бы отправлено два ответа.
Функция bot
просто регистрирует middleware, который получает все обновления. Именно поэтому session()
устанавливается через bot
- мы хотим, чтобы плагин работал со всеми обновлениями, независимо от того, какие данные в них содержатся.
Наличие стека middleware - чрезвычайно мощное свойство любого веб-фреймворка, и этот паттерн широко популярен (не только для ботов Telegram).
Давайте напишем свой собственный кусочек middleware, чтобы лучше проиллюстрировать, как это работает.
Writing Custom Middleware
Мы проиллюстрируем концепцию middleware, написав простую middleware функцию, которая может измерять время ответа вашего бота, то есть время, которое требуется вашему боту для обработки сообщения.
Вот сигнатура функции для нашего middleware. Вы можете сравнить ее с типом middleware, приведенным выше, и убедить себя в том, что у нас действительно есть middleware.
/** Измеряет время отклика бота и записывает его в `console` */
async function responseTime(
ctx: Context,
next: NextFunction, // аналог для: () => Promise<void>
): Promise<void> {
// TODO: реализовать
}
2
3
4
5
6
7
/** Измеряет время отклика бота и записывает его в `console` */
async function responseTime(ctx, next) {
// TODO: реализовать
}
2
3
4
Мы можем установить его в наш экземпляр bot
с помощью bot
:
bot.use(responseTime);
Давайте приступим к его реализации. Вот что мы хотим сделать:
- Как только приходит обновление, мы сохраняем
Date
в переменную..now() - Мы вызываем middleware, и, таким образом, позволяем обрабатывать все сообщения. Это включает в себя подбор команды, ответ и все остальное, что делает ваш бот.
- Мы снова берем
Date
, сравниваем его со старым значением и.now() console
выводит разницу во времени..log
Важно установить наш middleware response
первым на бота (в верхней части стека middleware), чтобы убедиться, что все операции будут включены в измерение.
/** Измеряет время отклика бота и записывает его в `console` */
async function responseTime(
ctx: Context,
next: NextFunction, // аналог для: () => Promise<void>
): Promise<void> {
// сохраните начальное
const before = Date.now(); // миллисекунды
// вызовите нижележащий middleware
await next(); // убедитесь что вы ждёте отработки
// сохраните конечное время
const after = Date.now(); // миллисекунды
// выведите разницу
console.log(`Время ответа: ${after - before} мс`);
}
bot.use(responseTime);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Измеряет время отклика бота и записывает его в `console` */
async function responseTime(ctx, next) {
// сохраните начальное
const before = Date.now(); // миллисекунды
// вызовите нижележащий middleware
await next(); // убедитесь что вы ждёте отработки
// сохраните конечное время
const after = Date.now(); // миллисекунды
// выведите разницу
console.log(`Время ответа: ${after - before} мс`);
}
bot.use(responseTime);
2
3
4
5
6
7
8
9
10
11
12
13
Готовый и работает! ✔️
Не стесняйтесь использовать этот middleware в своем объекте бота, регистрировать больше слушателей и играть с примером. Это поможет вам полностью понять, что такое middleware.
ОПАСНО: Всегда следите за тем, чтобы вы ожидали отработки!
Если вы вызовете next()
без ключевого слова await
, это приведет к нескольким поломкам:
- ❌ Ваш стек middleware будет выполняться в неправильном порядке.
- ❌ Возможна потеря данных.
- ❌ Некоторые сообщения могут быть не отправлены.
- ❌ Ваш бот может случайно упасть в результате трудно воспроизводимых действий.
- ❌ Если произойдет ошибка, ваш обработчик ошибок не будет вызван для нее. Вместо этого вы увидите, что возникнет
Unhandled
, что может привести к краху процесса вашего бота.Promise Rejection Warning - ❌ Сломается механизм обратного давления в grammY runner, который защищает ваш сервер от чрезмерно высокой нагрузки, например, во время скачков нагрузки.
- 💀 Иногда он также убивает всех ваших невинных котят :cry_cat_face:
Правило, согласно которому вы должны использовать await
, особенно важно для next()
, но на самом деле оно применимо к любому выражению, возвращающему Promise
. Сюда относятся bot
, ctx
и все остальные сетевые вызовы. Если ваш проект важен для вас, то вы используете инструменты линтинга, которые предупредят вас, если вы забудете использовать await
для Promise
.
Enable no-floating-promises
Рассмотрите возможность использования ESLint и настройте его на использование правила noawait
(при этом крича на вас).
Свойства Middleware в grammY
В grammY, middleware может возвращать Promise
(который будет ожидаться
), но оно также может быть синхронным.
В отличие от других систем middleware (например, от express
), вы не можете передавать значения ошибок в next
. next
не принимает никаких аргументов. Если вы хотите получить ошибку, вы можете просто вызвать
ошибку. Еще одно отличие заключается в том, что не имеет значения, сколько аргументов принимает ваш middleware: ()
будет обработано точно так же, как (ctx)
, или как (ctx
.
Существует два типа middleware: функции и объекты. Объекты middleware — это просто обертка для функций middleware. В основном они используются внутри системы, но иногда могут помогать сторонним библиотекам или использоваться в расширенных сценариях, например, в Composer:
const bot = new Bot("");
bot.use(/*...*/);
bot.use(/*...*/);
const composer = new Composer();
composer.use(/*...*/);
composer.use(/*...*/);
composer.use(/*...*/);
bot.use(composer); // composer это объект middleware!
bot.use(/*...*/);
bot.use(/*...*/);
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
Если вы хотите глубже изучить, как grammY middleware, ознакомьтесь с возможностями middleware в расширенном разделе документации.