Проміжні обробники (middleware)
Обробники оновлень, які передаються в bot
, bot
та інші методи, називаються проміжними обробниками. Хоча цілком правильно називати їх “обробниками оновлень”, спрощено можна ще називати їх просто “обробниками”.
У цьому розділі пояснюється, що таке проміжні обробники, а grammY використовується для прикладу, щоб проілюструвати, як їх можна використовувати. Якщо ви шукаєте конкретну документацію про те, що робить реалізацію проміжних обробників у grammY особливою, перегляньте можливості проміжних обробників у просунутій документації.
Стек проміжних обробників
Припустимо, ви написали такого бота:
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
Після надходження оновлення зі звичайним текстовим повідомленням, будуть виконані наступні кроки:
- Ви надсилаєте боту “Привіт!”.
- Проміжний обробник плагіну для роботи сесій отримує оновлення і виконує необхідні дії для забезпечення сесії.
- Оновлення буде перевірено на наявність команди
/start
, яка в даному випадку відсутня. - Оновлення буде перевірено на наявність команди
/help
, яка в даному випадку відсутня. - Оновлення буде перевірено на наявність тексту, що в данному випадку буде успішним.
- На рядку, позначеному
(*)
, буде викликано проміжний обробник, який обробить оновлення, відповівши"Текст!"
.
Оновлення не перевіряється на наявність фото, оскільки проміжний обробник на рядку (*)
вже обробив оновлення.
Тож, як це працює? Давайте дізнаємося.
Ви можете переглянути тип Middleware
у довідці grammY тут:
// Деякі параметри типу пропущено для спрощення.
type Middleware = MiddlewareFn | MiddlewareObj;
2
Ага! Проміжний обробник може бути функцією або обʼєктом. Поки що ми використовували лише функції ((ctx)
), тому давайте наразі залишимо обʼєкти проміжних обробників та заглибимося в тип Middleware
(довідка):
// Знову пропущено параметри типу для спрощення.
type MiddlewareFn = (ctx: Context, next: NextFunction) => MaybePromise<unknown>;
// де
type NextFunction = () => Promise<void>;
2
3
4
Отже, проміжний обробник приймає два параметри! Поки що ми використовували лише один: обʼєкт контексту ctx
. Ми вже знаємо, що таке ctx
, але ми також бачимо функцію next
. Для розуміння, що таке next
, ми маємо загалом розглянути як працюють проміжні обробники, які ви встановлюєте в свого бота.
Ви можете розглядати всі встановлені функції проміжних обробників як кілька рівнів, які розташовані один на одному. Перший проміжний обробник, у нашому прикладі це session
, є найвищим рівнем, тому він першим отримує кожне оновлення. Далі він може вирішити, чи завершити обробку оновлення, чи передати його на наступний рівень. В нашому випадку на наступному рівні обробник команди /start
. Для виклику наступного проміжного обробника, який часто називають нижнім проміжним обробником, використовується функція next
. Також це означає, що якщо ви не викличете next
у вашому проміжному обробнику, то усі наступні нижні рівні проміжних обробників не будуть викликані.
Цей стек функцій називається стеком проміжних обробників.
(ctx, next) => ... |
(ctx, next) => ... |—————вищий проміжний обробник відносно X
(ctx, next) => ... |
(ctx, next) => ... <— проміжний обробник X. Викликаємо `next`, щоб передати оновлення 🡳
(ctx, next) => ... |
(ctx, next) => ... |—————нижчий проміжний обробник відносно X
(ctx, next) => ... |
Повертаючись до попереднього прикладу, тепер ми знаємо, чому bot
навіть не перевірявся: проміжний обробник в 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" - Проміжний обробник
":
отримує оновлення та перевіряє текст, що завершується успішно, оскільки будь-які команди також є текстовими повідомленнями. Оновлення негайно обробляється першим проміжним обробником, і ваш бот відповідаєtext" Текст!
.
Повідомлення ніколи не буде перевірятися, чи містить воно команду /start
! Порядок, у якому ви реєструєте проміжні обробники, має значення, оскільки він визначає послідовність рівнів у стеку проміжних обробників. Ви можете вирішити проблему, помінявши місцями 3-й та 4-й рядок. Якщо ви викличете next
у 3-у рядку, то буде надіслано дві відповіді.
Метод bot
реєструє проміжний обробник, який отримує всі оновлення. Ось чому session()
встановлюється через bot
: ми хочемо, щоб плагін працював з усіма оновленнями, незалежно від даних оновлення.
Наявність стеку проміжних обробників є надзвичайно потужною властивістю будь-якого фреймворку, і цей шаблон широко популярний не лише для ботів Telegram.
Давайте напишемо наш власний невеликий проміжний обробник, щоб краще проілюструвати, як це працює.
Написання власного проміжного обробника
Ми проілюструємо концепцію проміжних обробників, написавши просту функцію проміжного обробника, яка може вимірювати час відповіді вашого бота, тобто скільки часу потрібно вашому боту, щоб обробити повідомлення.
Ось сигнатура функції для нашого проміжного обробника. Ви можете порівняти його з типом проміжного обробника, наведеного вище, і переконатися, що ми дійсно створили проміжний обробник.
/** Вимірюємо час відповіді бота та виводимо його у `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() - Ми викликаємо нижні проміжні обробники, отже, дозволяємо виконувати всю подальшу обробку повідомлень. Це включає обробку команд, відповідання та все інше, що робить ваш бот.
- Ми знову беремо
Date
, порівнюємо його зі старим значенням та використовуємо.now() console
для виведення різниці в часі в консоль..log
Дуже важливо встановити наш проміжний обробник response
у бота у самому початку, на верхній рівень стеку. Це важливо тому, що ми хочемо вимірювати час усіх операцій обробки оновлення.
/** Вимірюємо час відповіді бота та виводимо його у `console` */
async function responseTime(
ctx: Context,
next: NextFunction, // це псевдонім для: () => Promise<void>
): Promise<void> {
// зберігаємо поточний час до виконання
const before = Date.now(); // мілісекунд
// викликаємо нижні проміжні обробники
await next(); // переконуємося, що дочекалися (`await`)!
// зберігаємо поточний час після виконання
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(); // мілісекунд
// викликаємо нижні проміжні обробники
await next(); // переконуємося, що дочекалися (`await`)!
// зберігаємо поточний час після виконання
const after = Date.now(); // мілісекунд
// виводимо різницю у консоль
console.log(`Час обробки: ${after - before} мс`);
}
bot.use(responseTime);
2
3
4
5
6
7
8
9
10
11
12
13
Готово та працює! ✔️
Не соромтеся використовувати цей проміжний обробник на своєму обʼєкті бота, зареєструйте більше обробників і пограйтеся з прикладом. Це допоможе вам повністю зрозуміти, що таке проміжний обробник.
ЗАСТЕРЕЖЕННЯ: завжди чекайте виконання наступного проміжного обробника!
Якщо ви коли-небудь викличете next()
без ключового слова await
, дещо вийде з ладу:
- ❌ Ваш стек проміжних обробників буде виконано в неправильному порядку.
- ❌ Ви можете втратити дані.
- ❌ Деякі повідомлення можуть не надіслатися.
- ❌ Ваш бот може випадково аварійно завершити роботу у спосіб, який важко відтворити.
- ❌ Якщо сталася помилка, ваш обробник помилок не буде викликаний. Натомість ви побачите, що зʼявиться
Unhandled
, що може призвести до збою вашого бота.Promise Rejection Warning - ❌ Ламається механізм плагіну для конкурентності (runner), що захищає ваш сервер від перенавантаження, наприклад, під час різких сплесків навантаження.
- 💀 Іноді це також може вбити всіх ваших невинних котиків. 😿
Правило використання await
є особливо важливим для next()
, але насправді воно стосується будь-якого виразу, який повертає Promise
. Це включає bot
, ctx
та всі інші мережеві виклики. Якщо ваш проєкт важливий для вас, ви напевно використовуєте інструменти лінтингу, які попереджають вас, якщо ви забули використати await
на Promise
.
Увімкніть no-floating-promises
Розгляньте можливість використання ESLint і налаштуйте його на використання noawait
, адже ESLint крикне на вас.
Властивості проміжних обробників у grammY
У grammY проміжний обробник може повертати Promise
, якого слід дочекатися (await
), але він також може бути синхронним.
На відміну від інших систем проміжних обробників, таких як система express
, ви не можете передати значення помилок у next
. next
не приймає жодних аргументів. Якщо ви хочете зробити помилку, ви можете просто кинути (throw
) її. Ще одна відмінність полягає в тому, що не має значення, скільки аргументів приймає ваш проміжний обробник: ()
буде оброблятися точно як (ctx)
або як (ctx
.
Існує два типи проміжних обробників: функції та обʼєкти. Обʼєкти проміжного обробника є просто оболонкою для функцій проміжного обробника. Здебільшого вони використовуються внутрішньо, але іноді також можуть допомогти стороннім бібліотекам або використовуватися у складних випадках: наприклад, із Composer:
const bot = new Bot("");
bot.use(/*...*/);
bot.use(/*...*/);
const composer = new Composer();
composer.use(/*...*/);
composer.use(/*...*/);
composer.use(/*...*/);
bot.use(composer); // composer — обʼєкт проміжного обробника!
bot.use(/*...*/);
bot.use(/*...*/);
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
Якщо ви хочете глибше дізнатися, як grammY реалізує проміжні обробники, перегляньте можливості проміжних обробників у просунутій документації.