Всем доброго дня! Меня зовут Семен, в команде я отвечаю за работу с 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. При этом он не знает, как шина будет реагировать на них: это определяется конфигом. Добавляя дополнительный уровень абстракции в виде шины, мы уменьшаем связность кода и увеличиваем гибкость приложения.
Если есть вопросы или интересные кейсы - добро пожаловать в комментарии!
Комментарии (10)
slavaRomantsov
24.05.2023 21:26+1Привет! Спасибо за статью! Не было опыта наладить взаимодействия через бэк по веб-сокетам? Чтобы например хранить какую-то информацию. Допустим настройки пользователя (чтобы все было одинаково на разных платформах) ну или другие данные. То есть у нас есть 2 и более приложения и каждое из них устанавливает веб-сокет соединение с бэкендом и таким образом все работает без участия шины событий (ну или она будет выступать как дублирующей на случай медленного интернета)
TrueRomanus
24.05.2023 21:26Если каждое приложение будет устанавливать веб-сокет то сервер очень легко может прилечь а если открыть пару тройку вкладок одного и того же сайта где такое решение реализовано то проблема увеличиться как n*k где n это количество вкладок а k это количество приложений внутри каждой вкладки.
bezuhten Автор
24.05.2023 21:26Привет! Интересный вопрос. Не очень понятна мотивация походов на бэк для обмена данными двух микрофронтендов, которые работают в контексте одного приложения обертки.
TrueRomanus
24.05.2023 21:26+2"Более общим решением будет связать микрофронтенды через хост-приложение, например создав сервис, где мы опишем, какие МФ есть в приложении, и их API. Так сами микрофронтенды не будут знать о существовании друг друга, но хост-приложение должно уметь обрабатывать их запросы." мне кажется или это противоречит идее микрофронтендов? У Вас получается god-object "хост-приложение" которое обо всех все знает хотя по факту должно быть наоборот. "Хост-приложение" в теории вообще не должно ничего знать а просто загружать требуемые модули.
"В микрофронтенде мы используем метод отправки события и саму шину берем из хост-приложения через синглтон InjectionToken" начнем с того что браузер уже из коробки имеет реализацию событийно-ориентированного подхода. Также шина добавляет целый кластер новых проблем. Как понять что произошла ошибка в action-е и вообще он выполнился? Если не выполнился стоит ли попробовать повторить? Что если ошибка произошла в самой шине? Что если нужно не просто кейс по типу "кликнули на что-то и надо что-то выполнить" а сложнее по типу "дай мне информацию о то то о чем знаешь только ты"? Что если надо выполнять по цепочке несколько вызовов?
yuriy-bezrukov
24.05.2023 21:26God-object, прям в точку. Но как иначе без шины, вроде самый простой и логичный вариант?
Про то как понять, что произошла ошибка в Actions и видеть его прогресс так такие проблемы стали возникать с приходом web sockets. (появились дополнительные actions=)))
TrueRomanus
24.05.2023 21:26Шина да самый верный вариант, моя мысль была в том чтобы не изобретать велосипед беря какую-то библиотеку для шины а использовать DOM events в котором нет всех проблем которые я выше описал.
bezuhten Автор
24.05.2023 21:26На счет God-object TrueRomanus все правильно пишет, поэтому такое решение у нас не используется.
По поводу ошибок - в шину передается сервис логирования который например кидает ошибки в консоль или как-то еще обрабатывает, в зависимости от пожелания потребителя шины.
Про более сложный кейс не очень понял вопрос. Цепочку вызовов можно реализовать когда предыдущий экшн триггерит следующий.
TrueRomanus
24.05.2023 21:26Я имел ввиду когда два приложения могут делиться какими-то знаниями уникальными для друг друга. Т.е. есть два приложения - приложение с заказом карт и приложение с подарками. Допустим при заказе карты я получаю подарок и бизнес кейс что я перехожу с одного приложения на другое но при этом они вполне могут обменяться данными. Приложение подарков может получить информацию о карте (название например) и отобразить название карты и еще что-то плюс подарок. Простите если пример странный, пытался придумать что-то по Вашему профилю.
JordanCpp
Интересно пойдёт ли эволюция дальше. Микро сервис поделится на нано сервис и т. д
bezuhten Автор
вполне возможно) уже есть всякие edge функции