Как мы решили проблему общения микрофронтендов в приложении
Всем доброго дня! Меня зовут Семен, в команде я отвечаю за работу с Angular.
В последние годы в сети часто мелькает термин «микрофронтенд». Технология не новая, но и не слишком изученная, а кроме того, решения из коробки по ней появились относительно недавно, до этого каждый создавал что-то свое.
Внедряя МФ, разработчики сталкиваются с новыми проблемами. Один из таких челленджей возникает при разработке: как грамотно организовать передачу данных между микрофронтендами? Расскажу о нашем опыте и поделюсь решением для их общения.
Что такое микрофронтенд
Современные веб-приложения обрастают все большим количеством функций и часто превращаются в super apps, которые могут все: и еду заказать, и почту проверить, и фильм показать. Для пользователей это удобно, но такие монолиты становится все сложнее поддерживать и развивать. Тут нам на помощь приходит архитектура микрофронтендов, как когда-то к бэкендерам пришли микросервисы.
Впервые термин «микрофронтенд» упоминается в ThoughtWorks Technology Radar в 2016 году. Идея в том, чтобы рассматривать приложение как конструктор, собранный из модулей. Каждый модуль микрофронтенда — независимое приложение. Такой подход дает большую гибкость, значительно уменьшает связность кода, упрощает рефакторинг и даже позволяет разработать МФ на разных фреймворках и затем объединить их в одном приложении.
Чаще всего архитектура приложения на микрофронтендах выглядит как хост-приложение — обертка — и набор МФ, которые в него встраиваются. Хост-приложение отвечает за общую функциональность, загрузку и инициализацию МФ. В то время как МФ реализуют отдельные бизнес-функции. Получается конструктор «лего» с кирпичиками-микрофронтендами, где, имея много кирпичиков, мы можем собирать и деплоить разные по функционалу приложения.
В нашем приложении мы решили использовать Webpack Module Federation Plugin как одно из передовых решений для работы с МФ. Этот плагин позволяет собирать микрофронтенды как standalone-модули и затем импортировать их в runtime в приложение-хост. При этом работу по добавлению зависимостей плагин берет на себя. Например, если МФ не найдет нужной ему зависимости в хост-приложении, то скачает файл со своей версией и подключит ее.
Само приложение-хост — это контейнер для микрофронтендов с набором базовых функций и несколькими лэйаутами на выбор. Для успешной работы приложения его части должны уметь общаться друг с другом. Это легко выполнимо, когда приложение — монолит, ведь оно заранее знает все части, из которых состоит. В ситуации с микрофронтендами приложение-хост понятия не имеет, какие именно МФ будут в него загружены.
Представим ситуацию, когда два МФ хотят обменяться данными. Как можно это реализовать, если у нас не цельное приложение, а набор из разных модулей?
Первое, что приходит на ум, — позволить микрофронтендам взаимодействовать друг с другом напрямую.
Более общим решением будет связать микрофронтенды через хост-приложение, например создав сервис, где мы опишем, какие МФ есть в приложении, и их API. Так сами микрофронтенды не будут знать о существовании друг друга, но хост-приложение должно уметь обрабатывать их запросы.
Те, кто работал с бэкендом, скорее всего, уже догадались, как еще сильнее абстрагировать код друг от друга, чтобы никакие части приложения не зависели от окружения и могли легко переиспользоваться.
Паттерн «Шина событий»
Идея паттерна «Шина событий» проста: можно представить его как центральную радиостанцию, которая отправляет сигнал во все стороны, но получить его могут только те, у кого антенна настроена на нужную волну.
Это значит, что есть некий центральный класс и множество его подписчиков. Когда кто-то из подписчиков отправляет событие в шину, оно перенаправляется всем остальным подписчикам и они решают, стоит ли с ним взаимодействовать. Это позволяет наладить общение какого угодно числа компонентов системы, не делая их зависимыми друг от друга и сохраняя разделение ответственности (принципы loose coupling и separation of concerns).
В нашей базовой реализации шина перенаправляет события всем подписчикам, затем они сами фильтруют и обрабатывают их. Мы решили перенести этот функционал в шину, но так, чтобы не сильно потерять в гибкости.
Для этого создали конфиг в формате json-файла, приложение-хост загрузило его и передало шине. Для использования в конфиге добавили отдельные сущности событий, которые приходят в шину, и действий, которыми шина реагирует на события. Внутри конфига определили взаимосвязь event — actions.
Дополнительное разделение на event и action позволяет привязать несколько actions к одному event или переиспользовать один action на множество events. Другими словами, микрофронтенд не будет отдавать событие «Открой новую вкладку» или «Отправь сообщение в чат» — вместо этого будет «Пользователь нажал на кнопку с ID клиента». А в event-actions-конфиге мы к этому событию привяжем actions «Открой новую вкладку» и «Отправь сообщение в чат». Получается, событие отвечает на вопрос «что произошло», а action — на вопрос «что делать».
Сама шина реализована в виде npm-библиотеки. При подключении в хост-приложения шина получает от него json конфиги с допустимыми events и actions. Также хост и микрофронтенды добавляют в шину обработчики на каждый action. В микрофронтенде мы используем метод отправки события и саму шину берем из хост-приложения через синглтон InjectionToken.
На следующем примере ситуация, когда один action выполняется при разных events.
Что в итоге
Паттерн «Шины событий» давно и успешно используется в микросервисной архитектуре на бэкенде, и так как микрофронтенды — это фактически те же микросервисы, только на фронте, вполне логично, что шина хорошо подходит для решения проблем взаимодействия МФ.
Использование event-action-модели позволяет гибко настроить шину для любого набора микрофронтендов и хост-приложения. В такой схеме МФ знает, что в хост-приложении будет шина, и отправляет в нее некий заранее прописанный в нем набор events. При этом он не знает, как шина будет реагировать на них: это определяется конфигом. Добавляя дополнительный уровень абстракции в виде шины, мы уменьшаем связность кода и увеличиваем гибкость приложения.
Если есть вопросы или интересные кейсы - добро пожаловать в комментарии!