ФЛОРА - это цифровая платформа по управлению дилерским бизнесом со сложной бизнес-логикой. Над ней работают 25+ продуктовых команд и несколько платформенных. Фронтенд системы состоит из более чем 65 микрофронтендов (далее МФ), и их количество постоянно увеличивается: регулярно появляются новые направления, функционал расширяется. Каждый МФ (или группа МФ) может иметь свой роутинг, стейт, набор sharing-модулей и типов. Группы МФ объединяются в целые подсистемы со своим жизненным циклом и экосистемой. У каждого МФ свой релизный цикл: он деплоится независимо от других, без пересборки образа основного shell-хоста (далее flora-host). Ещё в ФЛОРА можно открывать несколько инстансов одного приложения — вкладки реализованы на уровне всей системы. В этой статье я расскажу об архитектурных решениях и о том, как мы живем в описанных выше условиях
Пара слов обо мне
Всем привет! Меня зовут Балин Михаил, являюсь фронтенд архитектором в РОЛЬФтех, работаю в компании около 4 лет, проектировал фронтенд архитектуру ФЛОРА, также являюсь старшим разработчиком и техлидом в нескольких командах.
В 2023 году ФЛОРА только начинала свое существование и состояла из 10-15 модулей, которые имели связанность на бизнес-уровне, что влекло за собой административные и технические сложности. Передо мной стояла задача сделать универсальное, масштабируемое решение, которое бы позволило разрабатывать командам без ограничений по функционалу, возможность использовать любые доступные решения и библиотеки.
Что такое ФЛОРА
На ФЛОРА работают сотрудники дилерских центров, мастера в сервис-центрах и другие специалисты. На момент написания статьи ФЛОРА тиражирована на 20 дилерских центров, до конца года система будет работать на всех ДЦ. Более подробно о истории ФЛОРА можете почитать из статьи моего коллеги Александра Алехина Бизнес‑логика жила в базе 27 лет: как мы вынимали ее из Oracle, не останавливая дилерскую сеть
Задача и ограничения
Как я уже написал выше, задача - создать фронтенд “бизнесово-сложной” системы, которая поддерживает мульти-инстанс любого компонента,возможность неограниченной вложенности компонентов, иметь независимый деплой, должен быть легко масштабируемым, иметь возможность откатываться на стабильную версию компонента без сложностей. Также важным требованием является то, что встраиваемые компоненты могут быть переиспользованы и должны подключаться с минимальными телодвижениям, а так же не иметь ограничений в технологиях.
На данный момент имеем такой стек:
база - vue, pinia, vue-router
@module-federation/vite, @module-federation/enhanced - для сборки приложения с поддержкой expose и подключение в рантайм,
centrifuge - для вебсокет коннекта
@flora/core - ядро системы и sharing модули (авторизация, глобальный стейт, шина событий, remote-плагины)
@flora/uikit - библиотека компонентов, реализующая дизайн-систему ФЛОРА
@flora/config - eslint, stylelint, commitlint конфигурации (единые правила разработки и кодстайл)
@flora/api-gen - библиотека генерации typescript функций и типов из swagger прокси листов
vitest, playwright - тестирование
vitepress - документация
Что рассматривали
Теперь подробнее расскажу об архитектуре. У нас она делится на глобальную и локальную. При проектировании глобальной архитектуры я рассматривал множество вариантов, но практически все они сводились к созданию полноценных микрофронтендов. Из-за количества компонентов системы и требований все монолитные конструкции практически сразу отпали на фоне риска производственного ада и потраченных нервов разработчиков, а также затягивания сроков релиза (у каждой команды свои сроки, разное количество fe-разработчиков и своя атмосфера внутри команды). Расскажу про типы архитектур, которые рассматривались:
- Монолит
Из плюсов отмечу что стандартное монолитное приложение банально проще разрабатывать, легко использовать общие компоненты и данные, в ide полноценный автокомплит и все живут в одном пространстве. Проще отлаживать взаимодействие частей приложения. Отсутствие проблем в типизации модулей.
С другой стороны, при таком количестве модулей и компонентов возникает чисто административная проблема — общий релиз. Ошибки в разных частях приложения блокируют деплой, а разработчик из одной команды физически не может править код в совершенно другой области: это приводит к потере контроля над модулями. Каждый релиз — это редеплой образа приложения, а релизов в день может быть от 10 до 30. На этом этапе уже стало понятно, что монолит — не наша история.
- iframe
Позволяет встроить готовое приложение без каких-либо изменений кода, стили изолированы, можно использовать любой стек. Но с учетом вкладок на уровне системы все ломается: каждая вкладка — это отдельный iframe со своим полноценным инстансом приложения, и уже один весомый iframe сильно нагружал систему. Плюс роутинг внутри iframe живет отдельно от адресной строки хоста
- single-spa
Почти идеальный вариант, но опять же не получилось адекватно сделать роутинг — например, активация приложений завязана на один глобальный URL, а нам нужно несколько инстансов одного приложения в разных вкладках, каждый со своим маршрутом>. Для нас — мизерные возможности в кастомизации.
- module-federation
Также почти идеальный вариант - он позволяет подключать не только один МФ целиком, но и отдельные модули в разных частях за счет expose. Также sharing библиотек работает шикарно.
В итоге остановился на module-federation - докрутили недостатки и адаптировали под наши реалии. При выборе данной библиотеки не решились проблемы с типизацией пропсов микрофронтенда. На нативном уровне это довольно сложно сделать, временным решением является минимизация количества передаваемых параметров. Вся остальная информация для компонента извлекается из глобального стейта или стейта подсистемы.
Локальная архитектура
Это почти всегда архитектура самого микрофронтенда. В 70% случаев - это модульная архитектура в чистом виде, как она есть. Модульность отлично ложится на мультиэкспоз подход за счет четкой и понятной структуры, а также настроенного public api в каждом модуле. Некоторые разработчики практикуют FSD в нескольких приложениях. Там где нет нескольких экспоузов это приемлемо. В совсем небольших приложениях зачастую отсутствуют какие либо виды архитектуры.
P.S Экспоз (expose) - это возможность микрофронтенда шарить модуль для разных потребителей. Мультиэкспоз - возможность шарить несколько модулей в рамках одного микрофронтенда.

Как устроен flora-host

Корнем всей системы является flora-host — хост-приложение, которое загружает МФ, занимается их оркестрацией, имеет единую точку авторизации пользователя, реализует глобальный роутинг и хранит глобальный стейт (данные пользователя, токены авторизации, метаинформацию, состояния вкладок). Сами МФ подключаются по заранее сгенерированному конфигурационному реестру (список ссылок на объекты в s3-бакете, api-ключи и другая метаинформация). Фактически в хосте находятся только шапка и вкладки. В основном контенте — только арена для загрузки МФ, которые подключаются по единым правилам.

Как хост находит и загружает модули

Каждый МФ в своем релизном цикле в итоге попадает в s3 бакет. У него есть там своя папка в которой хранятся сборки, разделенные поверсионно + latest - эта папка всегда указывает на последнюю загруженную версию микроприложения, нужно только для dev стенда. flora-host в образе получает сгенерированный конфигурационный файл и лениво подгружает чанк remoteEntry.js. Чанк состоит из точки монтирования, которая определяет какой именно экспоз отредерить. Сами чанки загружаются лениво и остаются в памяти до перезагрузки всего приложения. Плюсом конфигурации является быстрый откат на предыдущую версию, при наличии критических багов

Как модули общаются.
Вся архитектура завязана на однонаправленном потоке данных: родитель передает данные дочерним компонентам через пропсы, а в обратную сторону вызываются события через глобальную шину EventEmitter

События делятся на низкоуровневые и высокоуровневые. Низкоуровневые - это подкапотные манипуляция с вкладками и общим стейтом, ими занимается в основном flora-host (добавляет, удаляет, переименовывает вкладки, меняет урлы и get-параметры в стейте вкладок) - все то, о чем бизнесовый МФ не должен задумываться. Высокоуровневые - это уже больше передача минимальных бизнес-данных или триггер родительского хоста перезапросить определенные данные и записать их в глобальное хранилище.
P.S. Глобальный стор — это pinia-хранилище, которым управляет только flora-host. Оно защищено от прямой записи и имеет публичный api (userStore, crmStore, metaStore, tabStore и т. д.). При этом каждый МФ может создавать свои локальные сторы, которые действуют в рамках открытой вкладки. От обычного стора такой отличается только названием-идентификатором, например <id>-clientEntityStore. Уникальным идентификатором может быть id вкладки, useId() из инструментария vue или любое другое уникальное значение, присущее активной вкладке.
Что ломалось в процессе
В процессе разработки мы столкнулись с проблемой слишком большой вложенности компонентов. В самом сложном кейсе она доходила до 5–7 вложенных друг в друга МФ. Во-первых, это усложняло поиск компонента, в котором образовался баг, повлиявший на критически важные данные. Во-вторых, нужно было настраивать коммуникации и контракты между командами — было бы гораздо проще одной командой разработать решение целиком и выпустить его в прод. А так за таким модулем приходится особо пристально следить, причем нескольким командам; благо таких кейсов 1–2. Отмечу, что большая вложенность неизбежно увеличивает время рендера страницы (пока вся цепочка прогрузится, скачаются все чанки и отрендерятся), поэтому для таких систем важно иметь мощную инфраструктуру.
Следующая проблема - разработчику нужно много держать в голове много тонкостей работы его МФ внутри системы. Например, что существуют события шины, для того чтобы открыть новую вкладку или переименовать свою при переходе на другую страницу, что нужно использовать определенные обвязки для корректного функционирования и что работа его приложения может повлиять на состояние других приложений. Мы, конечно, ведем общую документацию, в которой расписаны подходы к разработке, api различных библиотек, архитектуре, возможностях каждого микроприложения и его типы - но в итоге мы встречаемся с кейсами, когда приходится старшим разработчикам “ходить” в другие команды для разрешения того или иного бага. На старте таких проблем было много, сейчас же встречается все реже.
Напоследок отмечу работу роутера. Vue-router из коробки не умеет в несколько одинаковых инстансов, поэтому его пришлось адаптировать под различные режимы работы. Например, автоматически подставлять уникальный идентификатор в url — это нужно, чтобы определить активную вкладку и подставить нужный компонент для рендера. Также я закладывал возможность иметь свой роутинг МФ на любом уровне вложенности, но это оказалось невозможно из-за архитектурных и физических ограничений — поэтому роутинг у нас максимум двухуровневый (глобальный в расчет не берем).
В данный момент, я пересматриваю работу роутера, а именно переход от webHistory режима к memoryHistory + pinia. Корректная работа webHistory в такой системе накладывает большие ограничения и заставляет делать большие обвязки вокруг самой библиотеки, я разрабатываю решение на основе роутинга в памяти самого приложения. Это позволит использовать роутинг в нативном виде, быстрее и качественнее реагировать на изменения роутов.
Кому это подходит
В конце хотел бы рассказать, кому подойдет данный вид архитектуры. В первую очередь — системам с требованиями к независимым релизам. Если ваша система требует независимой разработки и релиза каждого отдельного модуля (супераппы, комплексные решения с разными направлениями в бизнесе, порталы с различным функционалом), архитектура микрофронтендов с возможностью вложенности отлично подойдет. Она сочетает в себе полную независимость разработки на фронтенде и в то же время зависимость на уровне системного анализа и бэкенда. Также не забываем о возможности масштабироваться, не затрагивая другие модули.
В остальных случаях стоит воздержаться: разработка, поддержка и содержание такой архитектуры в инфраструктурном плане неизбежно будут увеличивать расходы компании.
Спасибо, что дочитали статью до конца! Буду рад обратной связи и могу подробнее рассказать о внутренностях фронтенда ФЛОРА в следующих статьях.
