Привет, Хабр! Этот пост подготовили два разработчика Росбанка — Максим из команды развития фронт-офисных систем и Никита из команды интернет-банка. Речь пойдет о том, как мы делаем микрофронтенды. Сначала расскажем про опыт интеграции в существующий проект, а потом про внедрение с нуля.
Микрофронтенды — это реализация микросервисного подхода для клиентских приложений; по факту, много маленьких независимых монолитов, которые взаимодействуют между собой. Их легко разрабатывать, развертывать, тестировать. При мерже возникает меньше конфликтов, так как релизы у всех независимые. Но при этом усложняется архитектура проектов. Также приходится дополнительно обдумывать взаимодействие микрофронтендов, их типизацию и документацию.
Внедрение микрофронтендов в готовый проект
Есть разные способы сделать это. Опишу те, которые рассматривали мы:
Iframe. Преимущества: изолированность и безопасность (для каждого микрофронтенда поднимается целая страница), простота внедрения. Недостатки: сравнительно высокое потребление ресурсов и ограниченное пространство отрисовки.
Рендеринг в разные теги на странице. Преимущество: можно использовать разные фреймворки. Недостаток: конкуренция стилей. Важно также учесть, что здесь используется одно окно — этот момент к плюсам и минусам однозначно не отнести.
Module Federation. Преимущества: легкость взаимодействия фронтендов и shared модули. Недостаток: пока можно использовать совместно только React и Vue.
Теперь о проектах. Приложение кол-центра Росбанка предназначено для обслуживания физических лиц, и мы решили туда же добавить возможность обслуживания лиц юридических. Разработкой занималась другая команда в том же репозитории. При мерже появлялось очень много конфликтов, условий и проблем с релизами. Одна команда заканчивала, другая не успевала. В итоге получали очень много лишних условий в коде, прямо if if’ом погонял.
Мы учли ошибки и ко второму подобному проекту — обслуживанию физлиц в дополнительных офисах — решили подойти более обстоятельно. Начали думать, как предотвратить проблемы, и из трех перечисленных выше вариантов пошли через Module Federation, благо в обеих командах проекта пишут на React+Redux.
На иллюстрации выше я скомпоновал конфигурационные файлы. Как видите, всего несколько строчек кода, и в одном проекте мы легко можем использовать модуль, выделенный из другого проекта. В данном случае это компонент React, но на его месте может быть функция, другой объект и вообще что угодно.
Сначала мы подключали микрофронтенд постранично. Скопировали основной репозиторий и убрали из него всю функциональность именно для физических лиц. Остались компоненты UI, надстройки, роутинг и другая логика. Затем развернули там второе приложение, для юридических лиц, и сделали надстройку, которая при переключении вкладок меняет страницу. Такой вот микрофронтенд на минималках:
Первые проблемы начались, когда в приложение для физлиц начали добавляться новые вкладки:
Чтобы то же самое появлялось и для юрлиц, нужно было добавить это вручную в репозиторий. Мы изначально неправильно выделили микрофронтенды — нужно было брать всю «шапку» целиком, чтобы она стала общей для обоих кейсов.
Затем к нам пришли коллеги, работающие с юрлицами. Они хотели перетащить в то же окно вкладки сотрудников, как это сделано у тех, кто работает с физлицами. Всего это порядка 30 вкладок с кучей таблиц и разных модалок.
Переносить вручную было неразумно, и мы пошли через микрофронтенд: взяли компонент, выделили его в отдельный модуль и подключили в другом приложении с помощью Module Federation. И тут же столкнулись с ошибкой: при шаринге компонента таким способом теряется контекст, например, routing или store из Redux. У нас есть множество самописных контекстов, которые отвечают здесь за ролевые модели пользователей. Они тоже пропали.
Эта проблема решается разными способами. Мы можем выделить контекстные провайдеры в отдельные модули и в каждом приложении использовать такой выделенный модуль. Со ссылкой на один и тот же инстанс контекста все заработает само собой. Но мы решили поступить чуть иначе: обернули компонент с вкладками во все имевшиеся контексты и в другом приложении через пропсы прокинули контекст для замены.
Тогда возникла другая проблема. Все наши компоненты берут данные из одного хранилища. В компоненте у нас была информация о пользователе, но в тот момент, когда мы его со всеми вкладками выделили в модуль, при отрисовке на странице в его хранилище этих данных ещё не было. И мы решили передавать хранилище через window.
Какие здесь есть варианты обмена данными между микрофронтендами?
PostMessage — используем его для iframe’ов.
Выделение хранилища в отдельный модуль — это позволит подключать его в каждом микрофронтенде, а затем уже отправлять данные.
Redux-micro-frontend — мы наткнулись на эту библиотеку, когда пытались скрестить два первых способа.
Вот так выглядит схема подключения микрофронтенда к глобальному хранилищу с помощью этой библиотеки. По сути, она добавляет в window инстанс глобального хранилища, где создаются хранилища для других микрофронтендов. И при создании нового хранилища нам возвращается инстанс этого хранилища из глобального хранилища. Так мы в итоге ссылаемся на один объект и можем подписаться на его изменения.
Чтобы общаться с другими микрофронтендами, у нас также дополнительно предусмотрен dispatch global action — своего рода PostMessage между айфреймами, но уже на базе Redux. Если, например, в схеме выше хранилище 2 поддерживает эти экшены, то оно их примет, данные изменятся, соответствующий микрофронтенд получит их и перерендерится.
Вот как в итоге выглядит схема микрофронтендов в проекте дополнительных офисов:
Внутри приложения мы выделяем компоненты в микрофронтенды и подсовываем им хранилище для работы. Хранилище мы регистрируем в хост-приложении. Затем, когда один микрофронтенд будет отрисовываться в странице хост-приложения, он получит данные из своего хранилища, как и остальные микрофронтенды.
В дальнейшем мы планируем разработать UI-kit и оформить всё это в сторибуке. Сейчас UI-компоненты разнесены на два репозитория с разными используемыми приложениями, и это нужно унифицировать. Кроме того, у нас есть проблема с типизацией: для сторонних микрофронтендов типы приходится передавать вручную. Для решения этой проблемы мы подключим динамическую типизацию. Также мы планируем вывести в отдельное хранилище повторяющиеся данные — в частности, информацию о юзерах и ролевой модели.
Внедрение в проект с нуля
Теперь о другом проекте. Из-за возможных санкций нам потребовалось разработать новый интернет-банк. Бэкенд при этом у нас уже был.
Использовали следующий стек: TypeScript, React, Mobx, CSS-модули. Взяли именно Mobx, потому что Redux надоел и хотелось больше объектно-ориентированного подхода в разработке состояния приложения и бизнес-логики. CSS-модули вместо БЭМ использовали, потому что не хотели проблем со стилями в разных репозиториях. Также смотрели в сторону сборщика Vite — хотя во многом он быстрее Webpack, но официально Module Federation разрабатывался именно для Webpack, и версия для Vite имела много проблем (хотя сейчас, возможно, их уже меньше).
Подход через микрофронтенды позволяет разрабатывать приложения с использованием разных репозиториев. Микрофронтенды дают возможность при будущем увеличении числа команд четко разграничивать их зоны ответственности.
Мы используем разные репозитории, и все разработчики могут подтягивать разные их наборы. Нам был необходим шаринг типов — его мы сделали через самописный плагин Webpack. Он генерирует и складирует типы своего микрофронтенда, а также скачивает необходимые типы внешних микрофронтендов. При скачивании он получает d.ts файлы — их можно легко импортировать и типизировать таким образом внешние микрофронтенды. При этом они могут скачиваться локально или с сервера. В любом случае у разработчика будут в распоряжении все нужные типы.
Также нам бы хотелось управлять загрузкой и фоновой подгрузкой компонентов. Suspense и Lazy нам не подходят. Вроде бы в React планируют добавить создание асинхронных компонентов, там можно будет отслеживать загрузку динамических импортов. Надеюсь, это нам поможет.
Еще одна наша потребность — роутинг и шаринг путей по всем микрофронтендам. Маршруты у них всех разные, как внешние, так и внутренние. Мы пробовали делать это на хосте, но попадали на циклическую зависимость и проблемы с типами. Отбросили этот вариант и вынесли всё в отдельный микрофронтенд. Внутренние маршруты у нас изолированы внутри компонента, а внешние хранятся в микрофронтенде и шарятся по приложению.
Мы стараемся поддерживать независимую разработку микрофронтендов. Единственное место, где допускаем пересечение — это UI-kit; там бывают редкие проблемы, которые решаются быстро. При наличии ошибок в коде Husky не позволит сделать коммит и покажет, в чем проблема.
Что хотелось бы добавить в будущем
Было бы здорово конфигурировать приоритет подгрузки микрофронтендов. Или иметь внешние библиотеки для этого, заточенные именно под Module Federation и React.
Также нужен нативный шаринг типов между микрофронтендами при использовании Typescript. В интернете я наткнулся на вопрос разработчику Module Federation: а что делать с типизацией на Typescript? Он ответил, что не разрабатывает на TS и, соответственно, с этим не сталкивался. В обсуждении увидели плагин для скачивания типов с сервера и сделали аналогичный.
И, наконец, шаринг стилей. У нас есть наборы стилей и функций, которые хотелось бы шарить, чтобы вытаскивать код препроцессора SCSS для последующей компиляции. Увы, Module Federation не умеет создавать библиотеки стилей, поэтому мы реализовали это через CSS и CDN.
Итоги
Учитывая все проблемы на пути, мы довольны результатом. Архитектура микрофронтендов работает стабильно, пока кто-нибудь не ошибется со ссылками. Тогда, конечно, возникают ошибки и приложение падает. Но это также опасно и для монолитной архитектуры. Module Federation еще предстоит обкатывать, но все проблемы кажутся вполне решаемыми. Мы ждем больше крутых фичей и надеемся, что этот подход со временем станет стандартом.
c_kotik
А между тем более года назад выходила такая вот статья. https://habr.com/ru/company/netcracker/blog/568054/