И сократил счет за серверную часть приложения
22 сентября 2026 г. · 7 мин чтения
Если вы следили за запутанной сагой о том, как команда Flutter решила заняться кофейнями, то знаете, что мы объединили Flutter, Firebase и Gemini для приготовления необычного кофе. Вы также знаете, что, поскольку мы ничего не брали за это, нам не удалось получить прибыль.
Более того, если вы заглянули в код, то могли заметить, что наш фронтенд на Flutter дополнялся функциями Firebase, написанными на Node.js. Это довольно удивительный исторический артефакт, учитывая, что поддержка функций Dart в Firebase появилась в публичном доступе почти одновременно с первым появлением GenLatte на Google Cloud Next.
Но в том-то и дело — поскольку поддержка Dart в Firebase появилась в самый последний момент, мы просто не могли позволить себе использовать ее для производственного приложения в условиях жесткого дедлайна. Если бы поддержка Dart в Firebase задержалась по какой-либо причине, мы могли бы не успеть развернуться к Cloud Next. И поэтому, когда GenLatte появился в апреле, мае или июне, он работал на бэкенде Node.js.
И я принял это на свой счет.
Полное погружение в fullstack Dart
Dart и JavaScript — разные языки с разными сильными сторонами. В каком-то смысле это очевидное утверждение, но оно также имеет более глубокие последствия для миграции на стороне сервера, поэтому простой перенос кода «один в один», вероятно, не стоит усилий. В конце концов, Dart на сервере может обеспечить сквозную безопасность типов вместе с Dart на клиенте, и было бы очень обидно оставаться с нетипизированными Maps.
Имея это в виду, и, возможно, переоценив свои силы, я решил кардинально переписать GenLatte. Среди моих многочисленных целей:
- Использовать общие модели во всем стеке (что было несложно, учитывая мое первоначальное решение поместить все классы данных в отдельный пакет genlatte_data)
- Сократить количество функций Firebase до одной развертываемой функции
- Удалить все операции записи на стороне клиента; вместо этого сосредоточиться на вызове серверных функций
- Удалить все серверные триггеры и вместо этого явно выполнять любые изменения данных в серверных функциях, вызываемых из клиента
- Сохранить все проверки ACL на основе ролей
- Наконец-то внедрить сквозное тестирование!
Эти цели были амбициозными и не совсем вписывались в мой план на 2026 год, поэтому, естественно, я держал свои планы в секрете и начал печатать!
Выполнение миграции
В отличие от работы над оригинальным GenLatte, которая в значительной степени велась без использования ИИ, я знал, что сжатые сроки требуют привлечения помощника по написанию кода. Моим выбором стала модель Gemini 3.6 Flash, и ее сочетание низкой задержки и общих знаний очень помогло мне.
В целом, я все же читал каждую строку, написанную Gemini, чтобы сохранить когнитивный контроль над проектом. Это было намного проще благодаря глубоким знаниям, и без этого обязательства я не думаю, что переписывание удалось бы за те несколько дней, что у меня были. Ассистенты по программированию, при всей их удивительности, все равно значительно выигрывают от человеческого руководства.
Общие модели
Безопасность типов во всем стеке означает общий код для клиента и сервера. Как упоминалось ранее, это означает выделение любого кода, который должен выполняться на сервере, в отдельный пакет, который, что критически важно, не должен зависеть от Flutter SDK. Это в значительной степени прямолинейно, но потребовало дополнений к моему собственному пакету управления данными, pkg:data_layer. Один из его вспомогательных пакетов, pkg:data_layer_firestore, зависит от клиентского Firebase SDK (который зависит от Flutter), поэтому я был вынужден добавить pkg:data_layer_firestore_admin, который является чистым Dart и, следовательно, удобен для сервера.
Использование одной функции Firebase
По умолчанию каждая развертываемая функция Firebase превращается в собственный сервис Cloud Run. (Если вы пользователь Firebase Functions и слышите об этом впервые, перейдите в Cloud Run в консоли Google Cloud и посмотрите, как делается «колбаса» Firebase!)
Однако, после использования более 15 отдельных сервисов Cloud Run в эпоху Node.js для GenLatte, я знал, что хочу более компактную настройку по нескольким причинам. Во-первых, развертывание было бы значительно быстрее, но, что еще важнее, это значительно сократило бы счет за сервер GenLatte.
«Но Крейг!» — скажете вы, — «Cloud Run сворачивается до нуля, когда не используется, так важно ли это?»
Отличный вопрос. Я рад, что вы внимательны.
И да, это очень важно! Пока GenLatte использовался, различные операции записи данных и асинхронные задачи запускали все 15 сервисов, и хотя каждый из них отключался в режиме ожидания, это все равно имело предсказуемые последствия для нашего счета за сервер. Но, что еще хуже, мы установили минимальное количество узлов для каждого сервиса равным 1, пока GenLatte был в использовании, чтобы избежать «холодных стартов», что, конечно, отключало функциональность сворачивания до нуля. Конечным результатом было то, что GenLatte был удивительно дорогим в запуске.
Как запихнуть все в один сервис Firebase
Чтобы сэкономить деньги, я решил вдохновиться Serverpod и внедрить DTO BackendMessage, чтобы сообщать моей единственной функции Firebase, какую внутреннюю функцию я хочу вызвать. С помощью некоторых хитрых приемов pkg:freezed, которые помогли мне написать Gemini, я даже смог получить типизированные ответы.
Финальная структура классов немного запутанная, но ее стоит понять, если вы любите безопасность типов и экономию денег.
BackendMessage
Это родительский класс DTO, который я использую в сигнатурах своих функций.
dart
MessageParameters
Он реализует BackendMessage и использует pkg:freezed для привязки отдельных типов сообщений к ожидаемым классам ответов.
Трюк @Implements.fromString создает классы, которые удовлетворяют требованию родителя привязывать тип MessageResponse. И хотя использование исходным классом необработанных строк кажется опасным с точки зрения типов, любые опечатки приводят к ошибкам в сгенерированных классах, что означает, что функционально это безопасно с точки зрения типов.
dart
MessageResponse
Это замыкает цикл, объявляя классы ответов, которые ожидает каждый вызов метода. Он предлагает сочетание отдельных типов ответов для методов, которым нужна немедленная обратная связь, и пустых ответов, представляющих своего рода HTTP-ответ 202 Accepted или 204 No Content.
dart
С моими DTO в порядке, мне нужна была единственная серверная функция для получения и маршрутизации каждого входящего BackendMessage соответствующим образом.
dart
Наконец, мне нужны были методы saveOrder и completeOrder, которые соответствовали бы их контракту.
dart
Благодаря этим системам я смог запихнуть произвольное количество серверных операций в одну функцию Dart. Привеееет, экономия!
Рефакторинг всех операций записи
Вышеупомянутая система BackendMessage создала основу для немедленного достижения еще трех моих подцелей:
- Я удалил все операции записи на стороне клиента, заблокировав их в файле firestore.rules. Я также переработал свои уровни управления данными, чтобы вызывать единственную бэкенд-функцию вместо прямого вызова функций Firestore, таких как docRef.set().
- Я аналогичным образом удалил все триггеры Firestore, но восстановил недостающую функциональность в функциях, которые я мог явно вызывать из клиента.
- Я сохранил строгую модель разрешений GenLatte, внедрив проверки ACL в коде Dart, что, как тестируемая система, помогло мне спать спокойно.
Добавление сквозных тестов
В качестве моего финального трюка мне удалось скомандовать Gemini написать как серверные тесты, так и, что очень здорово, имитировать поведение бэкенда в клиентских тестах, которые внезапно стали сквозными. Учитывая мои типы с гарантией безопасности, это привело к стабильности и надежности приложения, которые меня порадовали!
Радость от использования полностекового языка
Независимо от того, любите ли вы реляционные данные и поэтому используете что-то вроде Serverpod, или нереляционные решения вроде Cloud Firestore, писать на одном и том же языке для всего стека — это просто мечта. И преимуществ становится только больше, если этот язык — Dart, а вы наслаждаетесь полностью надежной и безопасной по отношению к null типизацией. Это действительно создает иллюзию, что компьютеры созданы для того, чтобы быть вашими друзьями!
Мне понравилось переводить GenLatte на повсеместное использование Dart, и благодаря этому счет за сервер сократился до крошечной доли от того, что было с Node, а надежность и производительность выросли. Деньги, потраченные на токены Gemini при выполнении этих изменений, с лихвой окупились уже после нескольких минут работы. Теперь GenLatte сможет еще долго радовать участников мероприятий персонализированным кофе или до тех пор, пока нам не надоест его поддерживать.









