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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В таком подходе до момента загрузки конфига хост и МФ не знают о событиях друг друга. На практике это означает, что для настройки поведения приложения достаточно понимания json. Например, технолог или бизнес-аналитик может сначала собрать приложение из доступных МФ и затем прописать в json, как они будут взаимодействовать между собой
В таком подходе до момента загрузки конфига хост и МФ не знают о событиях друг друга. На практике это означает, что для настройки поведения приложения достаточно понимания 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 и отобразит его
Пример 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
Получив событие cardError или procedureCriticalError, мы выполняем action sendErrorMessageToSlack, которое отправляет сообщение из параметра message в канал с ID channelId

Что в итоге 

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

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

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

Комментарии (10)


  1. JordanCpp
    24.05.2023 21:26

    Интересно пойдёт ли эволюция дальше. Микро сервис поделится на нано сервис и т. д


    1. bezuhten Автор
      24.05.2023 21:26

      вполне возможно) уже есть всякие edge функции


  1. slavaRomantsov
    24.05.2023 21:26
    +1

    Привет! Спасибо за статью! Не было опыта наладить взаимодействия через бэк по веб-сокетам? Чтобы например хранить какую-то информацию. Допустим настройки пользователя (чтобы все было одинаково на разных платформах) ну или другие данные. То есть у нас есть 2 и более приложения и каждое из них устанавливает веб-сокет соединение с бэкендом и таким образом все работает без участия шины событий (ну или она будет выступать как дублирующей на случай медленного интернета)


    1. TrueRomanus
      24.05.2023 21:26

      Если каждое приложение будет устанавливать веб-сокет то сервер очень легко может прилечь а если открыть пару тройку вкладок одного и того же сайта где такое решение реализовано то проблема увеличиться как n*k где n это количество вкладок а k это количество приложений внутри каждой вкладки.


    1. bezuhten Автор
      24.05.2023 21:26

      Привет! Интересный вопрос. Не очень понятна мотивация походов на бэк для обмена данными двух микрофронтендов, которые работают в контексте одного приложения обертки.


  1. TrueRomanus
    24.05.2023 21:26
    +2

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

    "В микрофронтенде мы используем метод отправки события и саму шину берем из хост-приложения через синглтон InjectionToken" начнем с того что браузер уже из коробки имеет реализацию событийно-ориентированного подхода. Также шина добавляет целый кластер новых проблем. Как понять что произошла ошибка в action-е и вообще он выполнился? Если не выполнился стоит ли попробовать повторить? Что если ошибка произошла в самой шине? Что если нужно не просто кейс по типу "кликнули на что-то и надо что-то выполнить" а сложнее по типу "дай мне информацию о то то о чем знаешь только ты"? Что если надо выполнять по цепочке несколько вызовов?


    1. yuriy-bezrukov
      24.05.2023 21:26

      God-object, прям в точку. Но как иначе без шины, вроде самый простой и логичный вариант?

      Про то как понять, что произошла ошибка в Actions и видеть его прогресс так такие проблемы стали возникать с приходом web sockets. (появились дополнительные actions=)))


      1. TrueRomanus
        24.05.2023 21:26

        Шина да самый верный вариант, моя мысль была в том чтобы не изобретать велосипед беря какую-то библиотеку для шины а использовать DOM events в котором нет всех проблем которые я выше описал.


    1. bezuhten Автор
      24.05.2023 21:26

      На счет God-object TrueRomanus все правильно пишет, поэтому такое решение у нас не используется.

      По поводу ошибок - в шину передается сервис логирования который например кидает ошибки в консоль или как-то еще обрабатывает, в зависимости от пожелания потребителя шины.

      Про более сложный кейс не очень понял вопрос. Цепочку вызовов можно реализовать когда предыдущий экшн триггерит следующий.


      1. TrueRomanus
        24.05.2023 21:26

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