Dispatch
- Глава
- 6 / 12
- Время
- 4 мин
- Код
- 12
Если ноды помогают подготовить данные, то Dispatch отвечает на другой вопрос: "куда вообще пойдёт событие и кто будет его обрабатывать?"
Когда бот маленький, об этом почти не думаешь. Есть @bot.on.message(...), есть хендлеры, всё работает. Но как только файлов становится больше одного, dispatch становится очень важным.
Самая короткая ментальная модель#
Можно держать в голове вот такую схему:
Dispatchсобирает маршрутизацию- внутри него живут
Router - внутри роутеров живут
View - внутри view живут хендлеры
То есть путь примерно такой:
Dispatch -> Router -> View -> Handler
Этой модели достаточно, чтобы не путаться дальше.
Что такое bot.on#
Когда вы пишете:
@bot.on.message(...)
async def handler(...): ...bot.on это и есть dispatch.
А bot.on.message это message-view основного роутера этого dispatch.
Проще говоря:
bot.on— главный диспетчер вашего ботаbot.on.message— место, куда вы складываете обработчики сообщенийbot.on.callback_query— место для callback query
Для старта этого понимания более чем достаточно.
Зачем вообще нужен Dispatch#
Чтобы не держать весь бот в одном файле.
Почти любой бот быстро дорастает до такого состояния:
- отдельные хендлеры для старта
- отдельные для админки
- отдельные для платежей
- отдельные для inline-кнопок
Если всё это держать в одном bot.py, через какое-то время в файле становится просто тяжело ориентироваться.
Dispatch решает это очень просто: вы делите бота на логические куски и потом собираете их вместе.
Минимальный dispatch#
from telegrinder import Dispatch, Message
from telegrinder.rules import Argument, Command, IsBot
dp = Dispatch(name="chat-utilities")
@dp.message(IsBot())
async def bot_message_handler(message: Message) -> None:
# Обычный обработчик, только регистрируем его не в bot.on, а в локальном dispatch.
await message.answer("Hey bot!")
@dp.message(
Command(
"repeat",
Argument("text"),
Argument("times", validators=[lambda s: int(s) if s.isdigit() else None], optional=True),
)
)
async def command_handler(message: Message, text: str, times: int = 5) -> None:
# В times попадёт либо число из команды, либо значение по умолчанию.
await message.answer(", ".join([text] * times))Такой dp уже можно вынести в отдельный файл и подключить к основному боту.
Как подключить dispatch к боту#
Например, пусть этот код лежит в handlers/chat_utilities.py.
Тогда сборка бота может выглядеть так:
from handlers import chat_utilities
from telegrinder import API, Telegrinder, Token
api = API(Token("your-token-here"))
bot = Telegrinder(api)
# Подгружаем все роутеры и error-handlers из внешнего dispatch.
bot.on.load(chat_utilities.dp)
bot.run_forever()На этом месте обычно приходит очень полезное ощущение: бот можно собирать из кусочков, а не писать как один длинный скрипт.
Где здесь роутеры#
Сейчас в telegrinder важно понимать, что dispatch работает не "напрямую с хендлерами", а через роутеры.
У dispatch есть:
main_routerrouters— очередь загруженных роутеров- views вроде
message,callback_query,inline_query,media_group,event_error,raw
Когда вы пишете:
@bot.on.message(Text("/start"))
async def start(...): ...по сути это регистрация в:
bot.on.main_router.messageТо есть основной роутер у вас уже есть, просто в обычной разработке его не всегда нужно трогать руками.
Что такое View#
View это объект для конкретного типа события.
Чаще всего вы будете видеть:
messagecallback_queryinline_querymedia_groupevent_errorraw
У view внутри есть:
- фильтр уровня view
- список хендлеров
- middleware
- waiter machine
Для новичка полезно воспринимать view как "полку, на которую я складываю обработчики одного типа".
Когда Router полезен отдельно#
Иногда хочется явно выделить отдельную логическую зону. Например, админку.
Тогда можно создать роутер руками:
from telegrinder import Dispatch, Message, Router
from telegrinder.rules import Text
admin_router = Router(name="admin")
@admin_router.message(Text("/ban"))
async def ban_handler(message: Message) -> None:
# Весь admin-related код можно держать в отдельном роутере.
await message.answer("Admin action")
admin = Dispatch(router=admin_router, name="admin-dispatch")А потом подключить его как обычный dispatch:
bot.on.load(admin)Такой подход особенно приятен, когда в проекте есть чёткие домены:
- админка
- платежи
- onboarding
- игры
- модерация
load_many и load_from_dir#
Когда частей становится больше, собирать бот можно ещё удобнее.
Несколько dispatch сразу#
bot.on.load_many(users.dp, payments.dp, admin.dp)Это полезно, когда вы уже импортировали нужные модули вручную и хотите собрать всё в одном месте.
Автозагрузка из директории#
bot.on.load_from_dir("handlers", recursive=True)Здесь telegrinder:
- проходит по Python-файлам
- импортирует их
- ищет глобальные переменные с экземплярами
Dispatch - подгружает их в основной dispatch
Это удобно для "blueprint-style" структуры проекта.
Вот минимальная идея:
# handlers/start.py
from telegrinder import Dispatch
dp = Dispatch(name="start")# bot.py
bot.on.load_from_dir("handlers", recursive=True)Как dispatch обрабатывает событие#
Без лишних деталей процесс примерно такой:
Dispatch.feed()получаетAPIиUpdate- создаётся
Context - запускаются middleware dispatch уровня
- dispatch проходит по своим роутерам
- каждый роутер проверяет подходящие views
- подходящий view запускает хендлеры
- если что-то падает, это можно обработать через
event_error
Новичку не нужно помнить все внутренности. Достаточно понимать, что dispatch это не просто "список функций", а довольно аккуратный конвейер.
Пример структуры проекта#
Вот хороший минимальный старт:
mybot/
bot.py
handlers/
start.py
admin.py
payments.py
keyboards/
menu.py
nodes/
db.py
rules/
is_admin.pyТакой проект растёт заметно спокойнее, чем один огромный файл на сотни строк.
Что запомнить#
Dispatchнужен для маршрутизации и разделения кода- внутри dispatch живут роутеры, а внутри роутеров живут views
bot.on.message(...)это по сути регистрация вmain_router.messageload,load_manyиload_from_dirпомогают собирать проект из нескольких частей- если бот начал расти, лучше разделять его раньше, а не позже