Всем доброго дня! Меня зовут Семен, в команде я отвечаю за работу с Angular.

В последние годы в сети часто мелькает термин «микрофронтенд». Технология не новая, но и не слишком изученная, а кроме того, решения из коробки по ней появились относительно недавно, до этого каждый создавал что-то свое. 

Внедряя МФ, разработчики сталкиваются с новыми проблемами. Один из таких челленджей возникает при разработке: как грамотно организовать передачу данных между микрофронтендами? Расскажу о нашем опыте и поделюсь решением для их общения. 

Что такое микрофронтенд

Современные веб-приложения обрастают все большим количеством функций и часто превращаются в super apps, которые могут все: и еду заказать, и почту проверить, и фильм показать. Для пользователей это удобно, но такие монолиты становится все сложнее поддерживать и развивать. Тут нам на помощь приходит архитектура микрофронтендов, как когда-то к бэкендерам пришли микросервисы.

Впервые термин «микрофронтенд» упоминается в ThoughtWorks Technology Radar в 2016 году. Идея в том, чтобы рассматривать приложение как конструктор, собранный из модулей. Каждый модуль микрофронтенда — независимое приложение. Такой подход дает большую гибкость, значительно уменьшает связность кода, упрощает рефакторинг и даже позволяет разработать МФ на разных фреймворках и затем объединить их в одном приложении. 

Чаще всего архитектура приложения на микрофронтендах выглядит как хост-приложение — обертка — и набор МФ, которые в него встраиваются. Хост-приложение отвечает за общую функциональность, загрузку и инициализацию МФ. В то время как МФ реализуют отдельные бизнес-функции. Получается конструктор «лего» с кирпичиками-микрофронтендами, где, имея много кирпичиков, мы можем собирать и деплоить разные по функционалу приложения. 

В нашем приложении мы решили использовать Webpack Module Federation Plugin как одно из передовых решений для работы с МФ. Этот плагин позволяет собирать микрофронтенды как standalone-модули и затем импортировать их в runtime в приложение-хост. При этом работу по добавлению зависимостей плагин берет на себя. Например, если МФ не найдет нужной ему зависимости в хост-приложении, то скачает файл со своей версией и подключит ее. 

Само приложение-хост — это контейнер для микрофронтендов с набором базовых функций и несколькими лэйаутами на выбор. Для успешной работы приложения его части должны уметь общаться друг с другом. Это легко выполнимо, когда приложение — монолит, ведь оно заранее знает все части, из которых состоит. В ситуации с микрофронтендами приложение-хост понятия не имеет, какие именно МФ будут в него загружены. 

Представим ситуацию, когда два МФ хотят обменяться данными. Как можно это реализовать, если у нас не цельное приложение, а набор из разных модулей?

Первое, что приходит на ум, — позволить микрофронтендам взаимодействовать друг с другом напрямую. 

Допустим, в приложении есть МФ с информацией по клиенту, он знает, что в том же приложении есть МФ чатов, и может отправить ему сообщение. Из плюсов — простота и прямолинейность такого решения, из минусов — фактически получаем такую же связность кода, как в монолите, и усложняем переиспользование микрофронтендов

Более общим решением будет связать микрофронтенды через хост-приложение, например создав сервис, где мы опишем, какие МФ есть в приложении, и их API. Так сами микрофронтенды не будут знать о существовании друг друга, но хост-приложение должно уметь обрабатывать их запросы. 

Этот подход добавляет уровень абстракции между МФ в виде сервиса, но оставляет необходимость подгонять хост-приложение под каждый набор микрофронтендов

Те, кто работал с бэкендом, скорее всего, уже догадались, как еще сильнее абстрагировать код друг от друга, чтобы никакие части приложения не зависели от окружения и могли легко переиспользоваться. 

Паттерн «Шина событий» 

Идея паттерна «Шина событий» проста: можно представить его как центральную радиостанцию, которая отправляет сигнал во все стороны, но получить его могут только те, у кого антенна настроена на нужную волну. 

Это значит, что есть некий центральный класс и множество его подписчиков. Когда кто-то из подписчиков отправляет событие в шину, оно перенаправляется всем остальным подписчикам и они решают, стоит ли с ним взаимодействовать. Это позволяет наладить общение какого угодно числа компонентов системы, не делая их зависимыми друг от друга и сохраняя разделение ответственности (принципы loose coupling и separation of concerns). 

В нашей базовой реализации шина перенаправляет события всем подписчикам, затем они сами фильтруют и обрабатывают их. Мы решили перенести этот функционал в шину, но так, чтобы не сильно потерять в гибкости. 

Для этого создали конфиг в формате json-файла, приложение-хост загрузило его и передало шине. Для использования в конфиге добавили отдельные сущности событий, которые приходят в шину, и действий, которыми шина реагирует на события. Внутри конфига определили взаимосвязь event — actions. 

В таком подходе до момента загрузки конфига хост и МФ не знают о событиях друг друга. На практике это означает, что для настройки поведения приложения достаточно понимания json. Например, технолог или бизнес-аналитик может сначала собрать приложение из доступных МФ и затем прописать в json, как они будут взаимодействовать между собой

Дополнительное разделение на event и action позволяет привязать несколько actions к одному event или переиспользовать один action на множество events. Другими словами, микрофронтенд не будет отдавать событие «Открой новую вкладку» или «Отправь сообщение в чат» — вместо этого будет «Пользователь нажал на кнопку с ID клиента». А в event-actions-конфиге мы к этому событию привяжем actions «Открой новую вкладку» и «Отправь сообщение в чат». Получается, событие отвечает на вопрос «что произошло», а action — на вопрос «что делать».

Сама шина реализована в виде npm-библиотеки. При подключении в хост-приложения шина получает от него json конфиги с допустимыми events и actions. Также хост и микрофронтенды добавляют в шину обработчики на каждый action. В микрофронтенде мы используем метод отправки события и саму шину берем из хост-приложения через синглтон InjectionToken. 

Пример event-actions-конфига. В нем на событие customerIdClicked от микрофронтенда customerMf привязаны два action. OpenMf откроет новый МФ, в параметре target указано, что этот action будет выполнен хост-приложением, в url — ссылка на файл МФ и в customerId — дополнительный параметр, который будет передан в новый открытый МФ. Еще там используется синтаксис json path $event.payload.customerId — это позволяет использовать переменные в json. В данном случае в переменной $event будет находиться информация по событию customerIdClicked. Следующим выполнится action sendMessageToChat — у него в поле target микрофронтенд чатов chatMf, который получит сообщение message и отобразит его

На следующем примере ситуация, когда один action выполняется при разных events.

Получив событие cardError или procedureCriticalError, мы выполняем action sendErrorMessageToSlack, которое отправляет сообщение из параметра message в канал с ID channelId

Что в итоге 

Паттерн «Шины событий» давно и успешно используется в микросервисной архитектуре на бэкенде, и так как микрофронтенды — это фактически те же микросервисы, только на фронте, вполне логично, что шина хорошо подходит для решения проблем взаимодействия МФ. 

Использование event-action-модели позволяет гибко настроить шину для любого набора микрофронтендов и хост-приложения. В такой схеме МФ знает, что в хост-приложении будет шина, и отправляет в нее некий заранее прописанный в нем набор events. При этом он не знает, как шина будет реагировать на них: это определяется конфигом. Добавляя дополнительный уровень абстракции в виде шины, мы уменьшаем связность кода и увеличиваем гибкость приложения.

Если есть вопросы или интересные кейсы - добро пожаловать в комментарии!