Telegram bot и Mini app на Dart. Боль?
Ни слова про Flutter, и да, боль.
А теперь перейдем к делу. Я Максим, и сейчас расскажу тебе, каково писать бэкенд и веб на Dart, когда ты мобильный разработчик.
Что делаем?
Бота для быстрого выбора времени встреч. Как Сalendly, но в Telegram.
Попробовать можно тут: t.me/TheRandevuBot
На чем пишем?
Можно было бы сделать как все нормальные люди: бэк на Python, приложение на каком-нибудь React. Да и задача у меня стоит именно сделать приложение, а не поиграться.
Но, как ты понял по названию, здравый смысл не восторжествовал, поэтому я выбрал Jaspr и Serverpod.
Jaspr — веб фреймворк, чтобы делать сайты на Dart, в стиле Flutter. Только виджеты здесь называются компонентами, и логика верстки из-за специфики HTML отличается.
Serverpod — веб сервер нового поколения, созданный специально для Flutter разработчиков. С world class logging, easy to use ORM, простейшим деплоем, и прочим маркетинговым булщитом.
Ты можешь спросить, а почему не Flutter? Я хотел, чтобы приложение не грузилось вечность, и было отзывчивым. Недавно Flutter команда разобралась с первой проблемой, но вторая все еще присутствует.
Так почему же эта пара? Serverpod действительно умеет делать чудесные вещи. Описываешь модели в yaml файле, пишешь класс Endpoint с нужными методами, а framework генерирует модели, репозитории для работы с базой данных, и API клиент с методами из Endpoint-а. А на другом конце у нас Jaspr, похожий на Flutter. На бумаге, да и на деле, степень переиспользования кода и знаний невероятная.
Переходим к разработке
Теперь самая жаркая часть. Давай поясню. В моем мире Flutter и радужных пони все вполне просто: платишь только за dev аккаунт, подчиняешься строгим и понятным правилам платформ, публикуешься во всем известных сторах. И вдруг я попадаю в сильно отличающийся мир web-а, устаревших API, Nginx-ов, Docker-ов, SSL сертификатов, и прочих Google cloud-ов.
Начнем с хостинга. Развернуть статический сайт можно в пару кликов. Наверняка так-же просто дела обстоят и сервером. Ну что там вообще сложного: зашел в GCP (Google Cloud Platform), выбрал виртуальную машину, закинул Docker image, запустил. Так ведь?
Даже Рик иногда ошибается
Ха ха ха. Наверное еще никогда в жизни я так не заблуждался, ведь на самом деле это выглядело так:
О, у serverpod есть гайд по деплою в GCP, а они еще и 300$ на три месяца дают, то что нужно! В гайде используется какой-то Terraform. Марс я покорять не намерен, поэтому сам найду как запустить приложение на сервере в Docker. Читаю гайд, создаю самую дешевую виртуалку за 7.5$ в месяц с аж 500MB оперативки и 0.25 (четвертью) ядра. Щедро, Google, щедро. Лаадно, через три месяца может что-нибудь другое найду. Google ведь серьезная компания и не зря такие деньги берут, наверняка в пару кликов сейчас все сделаю! (Oh, sweet summer child). И по началу действительно все шло хорошо: солнышко только взошло, я нашел нужную и хорошо написанную документацию, создал проект, включил и настроил нужные апи, разобрался в вариантах и настройках виртуальных машин, скачал gcloud cli, начал разбираться как залить Docker image, и заметил что уже поздний вечер… За день я хорошо потрудился, но по сути ничего не сделал. На следующий день продолжил, но быстро понял бесперспективность и пошел смотреть другие варианты.
Знакомый посоветовал Akamai. Солидные 25GB постоянной памяти, гигабайт оперативной, и ПОЛНОЦЕННОЕ ядро за 5$ звучат солидно. Воодушевленно начал регистрироваться: ввел карточку, номер телефона, личные данные, и получил бан. В принципе может и логично, за армянскую карту, российский паспорт и вьетнамский номер телефона и я бы себя забанил.
Ладно, дальше мой выбор пал на DigitalOcean. Хоть футболку они так и не прислали, зато VPN у них хорошо работал, а самое приятное, что они еще месяц мило просили меня заплатить, но не отключали сервер, когда российская карта перестала работать. (Кстати в итоге даже заплатил). Сделал по гайду виртуалку с Docker-ом, 1GB ОЗУ, 20GB ПЗУ и целым ядром, цена вопроса — 6$. На этом этапе решил сменить тактику, и по SSH закинуть на сервер репозиторий, собрать Docker image и запустить его.
С Docker я уже успешно работал. Что может пойти не так? Ну например Dockerfile от Serverpod может оказаться нерабочим. А еще image может собираться локально на MacOS, но падать с ошибкой на Linux, потому что Dart runtime на нем при сборке не копировался. Всего за 4 часа на SO, я разобрался что, откуда и зачем нужно брать из runtime в Dart image, и куда это добро класть. Люблю пол дня тратить на однострочный фикс. И в качестве вишенки на торте я прочувствовал всю разницу между expose и ports в Docker Compose.
Ненадолго перейдем от инфраструктуры к разработке. Я сделал веб приложение, написал интероп с Telegram JS API, захостил на Firebase. И разумеется у меня ничего с первого раза не заработало. А как дебажить, если Telegram, очевидно, не может обращаться к localhost? Слава всевышнему, я вспомнил как бэкендеры на работе запускали бэк локально, и давали мне ссылочку через какой-то Ngrok. Погуглил, и оказывается это то что нужно. Погуглил еще, и оказалось, что в VS Code есть встроенный, бесплатный, port forwarding. Запускаю на порту 8080 приложение, включаю forwarding этого порта, получаю ссылку, которая перенаправляет все запросу на мой localhost:8080. Просто и приятно. Но почему об этом совершенно нигде не упоминается? Ни в официальной документации от Telegram, ни в документации от сообщества, ни в туториалах на Medium! Мне повезло, я знал куда копать. А что делать менее опытному разработчику?
А теперь возвращаемся. Приложение отдебажено, бэк на сервере запускается. Домена нет, но и пофиг, пропишу напрямую IP и порт. Собираем все вместе, и знакомимся с великим и ужасным Content Security Policy. А дело все в том, что приложение отдается по HTTPS, а API работает по HTTP, и браузерам это совершенно не нравится. Что ж, может это можно отключить, или добавить нужный header, как с CORS? Можно с помощью meta tag попросить браузер закрыть глаза на это мелкое недоразумение. Но почему-то он не захотел слушаться. Придется и сервер переводить на HTTPS. Из-за Apple само слово сертификат вызывает у меня Вьетнамские флешбеки. Но что ж, ничего не поделать, как там получить SSL Certificate?
Туториал от DigitalOcean, да и здравый смысл, говорят, что неплохо было бы перед этим раздобыть домен. Там же подсказывают, что его можно получить бесплатно на freenome. На сайте у меня не получилось зарегистрироваться, и почитав Reddit я узнал, что это какой-то скам. Погуглив выяснил, что нет дураков раздавать бесплатные домены, обидно. Зато там же узнал про под домены, и про сервис Duckdns. Ребята сделали DNS на инфраструктуре Amazon, и бесплатно раздают поддомены, просто потому что могут! Окей, (sub)domain есть, связываем его с сервером, учимся работать с Nginx, и по прекрасному гайду за 15 минут устанавливаем certbot. Он сам создает сертификаты, настраивает Nginx для работы с HTTPS и следит за их обновлением.
И на этом, ты не поверишь, мои страдания с инфраструктурой закончились!
Dart на сервере и в вебе
Как оказалось, вне Flutter у Dart-а нет почти ничего. Пакет для аналитики? Либо последний commit два года назад, либо зависит от Flutter. Вход через Google? Ну ты ссылочку сам собери со всеми нужными параметрами, получи код, и, так уж и быть, можешь с ним авторизоваться используя пакет. Telegram bot? Есть пакет двухлетней давности, с устаревшим API, но в master ветке на GitHub находится более новая версия, о которой ты узнаешь сделав форк. А SDK для Rive анимаций, Telegram web app, CryptoCloud и многого другого вообще нет, пиши сам.
Jaspr
Он мне понравился. Тут и знакомый подход, с привычными виджетами, и SSR, и возможность подключить дизайн систему из JavaScript, и Tailwind.
Без минусов конечно не обошлось:
Встроенные компоненты, по сути теги. Они очень странные: почему-то сделаны в виде функций, может чтобы были с маленькой буквы, и почему-то обязательно принимают список дочерних компонентов, наверное тоже чтобы было похоже на HTML. В итоге часто делаешь тег, в который передаешь стили, и пустой список. Теги длиннее строки начинают выглядеть ужасно. Но это легко исправить, сделав удобную обертку в духе Flutter.
Скромный набор компонентов и пакетов. Нет ListenableBuilder, хотя есть ChangeNotifier, есть много тегов, сгенерированных по докам Mozilla, у некоторых даже есть дополнительные параметры, например у input есть name, value, type, enabled, onChange. Это приятно, но не богато. Есть навигация в стиле go_router, у нее даже в документации написано смотреть на flutter пакет! Есть riverpod, который я не люблю. А больше особо ничего и нет: ни bloc, ни provider, ни redux.
Еще есть система стилей. И это самая противоречивая часть.
С одной стороны они типизированы, разбиты по группам, что позволяет проще подобрать нужный стиль. К примеру через Styles.box
задаются отступы, размеры, позиция, а через Styles.text
задается стиль текста.
А с другой стороны у стилей, как и у многого другого, нет документации, что особенно неприятно. К примеру было бы очень удобно иметь описание для BoxSizing enum, чтобы знать что такое borderBox или contentBox, а не гуглить это по 5 раз, пока не запомнишь.
И если документацию легко исправить, то многословность сложнее. К примеру стиль с отступами и радиусом слева и сверху: Styles.box(padding: EdgeInsets.only(left: Unit.pixels(10), top: Unit.pixels(10)), radius: BorderRadius.only(bottomLeft: Radius.circular(Unit.pixels(10)), bottomRight: Radius.circular(Unit.pixels(10))))
Сравним с tailwind: pt-10 pl-10 rounded-bl-[10] rounded-br-[10]
Типизация это отлично, но пожалуй не такой ценой.
Мда, недостатков получилось больше. Но все равно Jaspr мне нравится. Он неплохо выполняет свои задачи. А может я предвзят из-за того, что над ним работает один человек, и я слежу за проектом с самого начала :)
Serverpod
По началу и он мне приглянулся. Он и правда хорошо генерирует шаблонный код: API клиент, репозитории, модели. Плюс предоставляет широкий функционал из коробки: авторизацию через email, Google, Apple, веб сервер для файлов, какой никакой движок HTML темплейтов, логгер, интеграцию с cloud provider-ами, VS Code extension для описания моделей в yaml. И все это с документацией и туториалами. Пожалуй все нужное для маленького проекта имеется.
И все бы хорошо, но меня очень сильно отталкивают две вещи:
Агрессивный маркетинг. К примеру они пишут про world class logging. На деле есть метод log с несколькими уровнями логгирования, записи сохраняются в базу, и их можно увидеть в приложении компаньоне Serverpod Insights. Вот только этот метод можно вызвать лишь в сессии, то есть во время выполнения запроса клиента. При инициализации сервера сессии нет, как и при обработке запроса от Telegram. А как отправлять логи ошибок в Sentry, или экспортировать их в файл? Правильно, никак. Получается логгирование есть, но полноценно использовать его нельзя, замечательно.
Архитектура и API. Добро пожаловать в мир инициализации и синхронного чтения из файла при создании объекта. Модульность никакая. Шаг в лево — костыль, шаг в право — хак и низкоуровневые API. Про логгирование уже сказал, с паролями все так же: используешь passwords.yaml, либо используешь passwords.yaml. С тестированием все плохо, никакого специального фреймворка нет. Готовься поднимать Postgres, создавать сессию и запускать сервер целиком.
Чуете? Попахивает GetX. В общем с одной стороны я бы использовал Serverpod только для проверки гипотез и мелких приложений, по крайней мере пока его хорошенько не отрефакторят. Ведь с помощью Shelf, Drift и gRPC можно сделать даже лучше. А с другой стороны все равно получится линукс: ты можешь все настроить и ты будешь все настраивать.
Заключение
С Dart можно решить любую задачу! Или наоборот, Full Stack на Dart для сильных духом. И то, и другое верно, выбирай сам что тебе ближе.
А лично я сильно прокачался и получил огромное удовольствие от создания бота, настройки сервера, верстки сайта и написания этой статьи. Надеюсь и тебе было интересно, спасибо за внимание!
P.S. Не забудь попробовать бота: t.me/TheRandevuBot:)