Сегодня я расскажу про кейс переезда, который до сих пор продолжается, — переезда Яндекс.Еды на микрофронтендовую технологию. Зачем мы это сделали, почему у нас это получилось здорово и что нам помогает делать это гораздо быстрее.
Как работают эксперименты и проверка гипотез
Про то, как работают эксперименты и проверка гипотез, расскажу чуть позднее. Как правило, у бигтехов есть конфигуратор, некая «экспериментница» — история, которая позволяет на часть пользователей раскатить какой-то функционал. Как правило, это фича-флаги. Это работает на всех фронтовых платформах, на всех бэкенд-платформах — неважно.
То есть эта штука позволяет, например, раскатить фичу на 50% пользователей, на 30%, или на людей, которые находятся в России, в Казахстане — раскатить какой-то конкретный кусок. Она позволяет гибко конфигурировать приложение в зависимости от обстоятельств и условий.
Почему мы пришли к микрофронтендам
Почему вообще мы пришли к микрофронтендам, зачем нам это оказалось нужно? На самом деле в международном направлении, в особенности в странах Африки, в которых Яндекс работает, — это Кот-д'Ивуар, Замбия и подобные страны — очень плохой мобильный интернет. Наш банк достаточно тяжёлый, честно, я не помню до конца, сколько он весит, но грузить его — это прямо больно. У нас даже ребята ездили в Африку и старались там оптимизировать.
В том году или позапрошлом я выходил на фронтенд-конференцию, и ребята из Мегамаркета рассказывали, как они на разных своих продуктах переиспользуют микрофронтенды. Мне показалось это классным решением в целом для Яндекса. Я притащил это в команду платформы, и мы начали это потихоньку делать. Тогда ещё AI не была такой популярной и не могла выполнять такой пласт задач, который может сделать сейчас.
Что даёт микрофронтенд помимо переиспользования
Поскольку у нас крупный продукт, релизы происходят раз в неделю, отвод релизной ветки — тоже раз в неделю. И когда каким-то образом случается так, что в проде оказывается что-то очень плохое, откатывается вся функциональность, которая была за эту неделю отведена. Это могут быть какие-то срочные фичи и так далее. Откат занимает тоже некоторое количество времени.
Вот буквально сравнение того, что мы делаем в монолите, когда релизимся, и что мы делаем в микрофронте:
- Частота релизов: в монолите — раз в неделю, в микрофронте — сколько душе угодно.
- Длительность выхода: от двух часов в монолите — это полный раскат на что-то типа 90 подов; в микрофронте — это буквально загрузка в S3-хранилище. Об этом чуть позднее расскажу.
- Зона поражения при ошибке: в монолите — деградация всей отведённой функциональности; в микрофронте, соответственно, узкая зона ответственности.
- Откат: в монолите занимает около двух часов вместе с откатом всех этих 90 подов; в микрофронте — по щелчку пальца. Об этом тоже расскажу, как мы это сделали.
- Регресс и код-фриз: при релизе монолита есть дежурные QA-инженеры, которые буквально шерстят весь функционал с каждым отводом и смотрят, ничего ли плохого не случилось. В микрофронте достаточно ответственной команде просто посмотреть ту конкретную часть, которая была изменена, и она спокойно убегает в прод, никого не ломая.
Как устроена работа с микрофронтами
У нас есть базово три части: монолит, JS микрофронта и какие-то типы, которые мы продолжаем интегрировать.
Как мы воспользовались нашей системой экспериментов и конфигураций, чтобы сделать очень красиво? Во время публикации микрофронта мы буквально загружаем его в S3-хранилище со ссылкой на файл. Ссылка представляет собой название микрофронта, точка, версия. И S3 может хранить очень большое количество версий — буквально сколько угодно.
В тот момент, когда CI говорит о том, что микрофронт зарелизился, автоматика сама создаёт драфт на изменение технического конфига, который просто буквально меняет версию. О чём я говорил, когда упоминал мгновенный откат: если что-то не так, буквально заходит SRE-инженер в этот конфиг, меняет версию назад — и всё, старая версия микрофронта на месте. Это просто происходит моментально.
А все типы, все штуки, которые должны использоваться за пределами этого микрофронта — например, другими микрофронтами или монолитом, — выплёвываются в пакет в npm, и он скачивается и ставится. Тут тоже всё просто.
Работа с версионностью
Вот пример небольшой того, как у нас происходит работа с версионностью. Здесь можно увидеть, что мы тащим эксперимент по конкретному микрофронту, тащим эксперимент по конфигу, версии фронта, и кешируем. Если он закеширован — соответственно, отдаём. Мы считаем, что во время работы с приложением необязательно обновлять версию: после полной перезагрузки закешированных значений не будет. Из контейнера достаём, кастим его и импортим через технологию Module Federation.
Наружу выплёвывается регистр в случае, если этот микрофронт подтянулся.
Проброс общих частей
По поводу того, как мы пробрасываем в микрофронтенд какие-то общие части. У нас, например, темы, эксперименты и так далее хранятся в монолите. И в микрофронтенд набрасывается только узкая часть сервисов, которая только ему и нужна.
Вот пример одного из таких сервисов: здесь можно увидеть темы, конфигурацию, переводы, хуки, аналитику, диплинк-менеджеры — какое-то количество сервисов. Это пример того, как двигается вот этот сервис, который мы потом используем, чтобы пробрасывать его в микрофронты, которые за них.
Масштаб сегодня
Сегодня у нас около 20 микрофронтов в проде, но это на самом деле не так много, учитывая, какая огромная еда в целом.
Забыл сказать: помимо того, что мы просто можем выделить ка-то зону микрофронта, мы можем ещё и указать от энтрипоинты. Например, есть модалка адресов, которая используется во всём приложении. Она лежит отдельным энтрипоинтом. То есть микрофронт лежит в одном репозитории, но энтрипоинт для конкретно этой модалки лежит наружу отдельно, его можно отдельно включить.
Соответственно, у нас сейчас пока что два репозитория — это монолит и репозиторий, который мы называем «фастфудом» (типа он быстрее). Ну и остался план-хвост монолита, который потихонечку худеет.
Переезд: с чего начинается
Скука начинается с переезда. Шаги у нас, на самом деле, одни и те же. Мы разделили процесс на два этапа, выделили одинаковые шаги и как раз здесь завели автоматизацию.
Что происходит в первой части?
- Создаётся каркас микрофронта — это делает скриптик, очень легко.
- Создаётся эксперимент в «экспериментнице», о которой я говорил.
- Создаётся скоуп типов.
- Создаётся свитчер на уровне монолита, который по эксперименту говорит: брать эту функциональность из монолитной части или из микрофронта.
- Роуты, npm-зависимости, спецификации экспериментов и всякие переводы.
В конце это два пул-реквеста в два репозитория: фастфуд и монолит.
По итогу первого этапа у нас есть микрофронт, который на самом деле просто содержит заглушку. Есть свитчер, который на уровне монолита переключает монолитную часть либо микрофронт-часть. Есть все типы, которые нужны, чтобы работал этот микрофронт. И какая-то инфраструктура вокруг в виде экспериментов, которая позволяет по щелчку пальцев переключать версии.
Автоматизация первого этапа
Расскажу, как мы автоматизировали первый этап и как нам на втором этапе нейронки всё ещё помогают.
Для первого этапа у нас есть скилл — генерация микрофронта. Что он делает? Как раз генерирует вот этот каркас с какими-то дефолтными файлами — tsconfig и так далее. Короче, просто каркас, чтобы потом в него писать.
Автоматически через MCP (MCP знаете все, да, что такое? Забыл спросить) создаётся эксперимент в наших конфигураторах. Потом создаются в рамках этого микрофронта общие типы, и на уровне монолита рождается этот переключатель, который смотрит на конфиг и выбирает, куда что включить — микрофронт или оставить в монолите. Грузятся какие-то первоначальные дефолтные роуты и генерируются эксперименты для этой конфигурации.
У каждого микрофронта теперь есть только те эксперименты, которые для него нужны. Раньше в монолите было какое-то количество экспериментов, а теперь каждый микрофронт инкапсулирует в себе только нужные и подсасывает именно их.
По результату выполнения этого скилла у нас буквально создаётся два пул-реквеста: один — в Фастфуд, с новым микрофронтом, второй — в монолит, с переключателем и с пробросами некоторых сервисов. Человеку остаётся только оформить эксперимент, который создался, и завести ключи в бункере.
Блин, забыл про бункер упомянуть. Бункер — это наша внутренняя технология, которая позволяет подсасывать переводы для конкретного интерфейса.
Этап два: куда уносить код
Второй этап достаточно нетривиальный, от раза к разу меняется. Нужно выделить границы того, какие зависимости в модуле есть — какие компоненты, какие данные для хранилища и так далее. Очень многое переиспользовать на самом деле не всегда удаётся. Но при этом мы стараемся переиспользовать то, что в других микрофронтах уже выносилось, и не дублировать код между микрофронтами.
Компонент из монолита тянет за собой несколько штук. Первое — это общие компоненты, хуки, типа инпутов, всяких useScope, useDispose. Какие-то штуки, которые на самом деле можно унести во что-то внешнее, и потом другие микрофронты это тоже смогут использовать.
Соответственно, наша задача — проанализировать то, что можно вынести в общее место, что уже вынес кто-то до нас, и мы можем это оттуда взять и переиспользовать; и что нужно сделать так, чтобы внутри микрофронта была какая-то сущность, которая инкапсулирует свои типы и свои зависимости наружу, чтобы другие микрофронты и прод могли их потребить.
Как помогает AI на втором этапе
Вот это нам и помогает на втором этапе сделать AI. Она буквально смотрит во весь репозиторий, анализирует все микрофронты, смотрит на лучшие практики, импортирует что-то, создаёт какие-то конкретные вещи, которые нигде не используются, в папку shared. Как раз то, что находится в папке shared, у нас публикуется во внутреннем npm и потом потребляется другими микрофронтами или прод-приложениями.
Как итог, по работе AI мы получаем какую-то карту зависимостей. Как правило, мы используем ещё что-нибудь типа progress.md, чтобы нейронка напоминала себе шаг за шагом, что она делает в рамках работы. Потому что одним промптом вынести весь клубок зависимостей очень сложно. Поскольку у нейронки ограниченный контекст, мы складываем это в файл, куда она потом заходит, читает и смотрит прогресс.
Как итог, мы получаем карту зависимостей, черновой импорт, перенос UI и импортов, адаптацию стилей, подготовку энтрипоинтов. Всё это скорее как план. Она также смотрит, что сделали уже соседние микрофронты: что мы можем переиспользовать, а что не нужно копировать.
Скилл для выноса общего: виджеты
Одна из зон ответственности — написали скилл для выноса того, что является общим. У нас есть такая история, как виджеты — их на самом деле десятки. Виджеты — это то, что вы видите всегда, когда заходите в Яндекс.Еду на главную: поиск, выдача, фильтры — это всё виджеты, которые конфигурируемы. У них примерно одинаковая структура зависимостей и структура работы.
Ребята из команды выбора в нашем случае сделали скилл, который буквально автоматизирует переход всех виджетов. Человеку остаётся только отревьюить, сделать небольшой diff-тест на стенде, отдать QA, чтобы он проверил. И в целом всё. Вот тут как раз хороший пример того, как мы можем с этим работать, потому что эта штука прямо общая.
На какие грабли мы натыкаемся
- Импорт банда соседа. У нас стоят жёсткие ограничения на то, чтобы один микрофронтенд импортил другой микрофронтенд именно JS-частью. Монолит выступает как мост для общения между микрофронтами, и мы поставили жёсткие ограничения, чтобы один микрофронт не использовал другой.
- Дублинг компонентов. Это как раз то, о чём я говорил: когда работают три команды параллельно, они выносят свою зону ответственности, и у них буквально в UI заносится три одинаковых компонента, потому что каждый используется в каждом из этих микрофронтов. Это сейчас происходит, но мы с этим боремся аккуратно.
- Сторы не переезжают. Как бы хорошо мы ни хотели инкапсулировать зону ответственности, сторы у нас зашиты в монолитной части приложения. Однако мы сделали отдельный фастфуд MFStore, который в себе инкапсулирует модели данных. То есть он создаёт вам просто модельки — вернее, не создаёт, а описывает интерфейс, скажем так. А создаёт сами модельки непосредственно монолит. И монолит пробрасывает сервисами в каждый его фронтенд эти модельки, там с ними происходит какое-то взаимодействие, методами происходит изменение и наполнение стора в монолите, и он уже отправляет данные на остальные микрофронты. Это всё реализовано через подписку условно.
- Чистка экспериментов. Последний скилл в этом всём — это чистка прямых экспериментов. Когда мы понимаем, что спокойно живём в микрофронте и нам не нужна эта часть в монолите, не нужен свитчер, мы просто говорим: всё, нам больше не нужен эксперимент, почисти всё, убери свитчер, теперь всегда используй данные из микрофронта. Это тоже мы автоматизируем. Пусть это не такая большая работа, чтобы удалить, — просто это рутина, которая из раза в раз одинаково повторяется.
Что остаётся человеку
За агентом — каркасы, зачистка экспов, типы, спеки, PR, комментарии, автотесты.
Человеку остаётся определить, что переезжает и когда переезжает, а также провести качественное код-ревью того, что делают нейронки. Это очень важно, потому что путь к абсолютному доверию к нейронке — это путь к багу в коде просто каждый день.
Также за человеком — пуш экспериментов в код, который обновляет версии, схемы конфигов и переводов в бункере, и какие-то архитектурные границы микрофронта, которые тоже описывает разработчик.