Привет! Меня зовут Алексей. Я занимаюсь проектированием фронтенд-составляющей ИТ-систем в архитектурном комитете SimbirSoft. Последние два-три года во фронтенд-сообществе активно обсуждается и используется термин «микрофронтенд» (далее МФ). Разные компании делятся своими подходами к организации подобного архитектурного решения, но до сих пор в Сети мало описания проблематики, которую призваны решить МФ-ы, критерии их применимости и ограничения в использовании. В этой статье постарался сравнить разные способы организации МФ-ов, а также сформировать рекомендации, где какой подход использовать.
Материал может быть полезен как аналитикам и командам разработки при проектировании архитектуры на проекте и закладки процессов, так и владельцам продуктов, поскольку внедрение МФ-ов может дать более управляемую разработку.
Микрофронтовый подход: что это и для чего нужен
Перед тем как перейдем к определению МФ-а, давайте рассмотрим несколько проблем, которые можно встретить на проектах:
У вас большой проект. Величина проекта, как правило, субъективная и может быть эмпирически определена объемом функционала и количеством разработчиков. Если у вас хватает работы, чтобы озадачить 1-2 фронтендеров и они при этом не будут «толкаться локтями» – это маленький проект, 3-6 – средний, больше 6-8 – уже большой.
У вас большая команда. Опять же эмпирически это больше 10 фронтендеров, остальные участники не в счет. Как правило, команда такой численности уже может разделиться на подкоманды, которые берут на поддержку конкретный функционал, обзаводятся своими аналитиками, бэкендерами и QA.
У вас большой функционал. Отдельно взятый разработчик может поддерживать только кусок кода своей подкоманды. Доработка остального кода может обходиться дорого в силу незнания предметной области или сложности реализации сторонней логики.
Проблемы могут дополнительно усиливаться:
желанием сменить стек;
требованием компании поддерживать семейство родственных проектов.
Как организовать взаимоотношения между таким количеством людей? Как выстроить процессы на проекте такого масштаба? Как грамотно разграничить зоны ответственности? Чем больше вы собрали проблем, тем сильнее стоит задуматься над внедрением микрофронтового подхода. Поскольку это является естественным продолжением эволюционного тренда в разработке на декомпозицию кода и команды проекта.
Таким образом, микрофронтовый подход – разделение монолитного фронта на отдельные кодовые базы, хранящиеся в отдельных репозиториях, к которым есть доступ у отдельных подкоманд. При этом у них могут/должны быть свои демо стенды, тесты и релизный цикл. Соответственно, микрофронт – отделяемый кусок интерфейса. Делить не обязательно постранично, функционал может быть и сквозным (например, корпоративный ui-kit). Отдельно стоит подсветить, что МФ-ы – это больше организационное решение о том, как управлять сложностью разработки в большом проекте. МФ-ы не помогут вам ускорить фронтенд, некоторые реализации наоборот даже замедлят. Но такой подход ускорит саму разработку за счет выделения зон ответственности и изолированного тестирования.
MF vs LL
Отдельно стоит упомянуть Lazy loading в сравнении с МФ-ами. Они решают разные задачи, но люди иногда думают, что это все об одном, ведь в обоих случаях мы «дробим» приложение.
Lazy loading решает проблему производительности: как не заставлять пользователя грузить весь фронтовый бандл целиком, как не ждать больше, чем нужно, как быстрее запустить фронт на клиенте и скорее начать с ним взаимодействовать.
МФ-ы проблему производительности не решают, а иногда даже усугубляют. Зато они помогают организовать разработку в более комфортном для конкретной подкоманды виде, минимизируя вышеперечисленные проблемы.
Buildtime vs Runtime
Теперь поговорим о подходе объединения МФ-ов в единое приложение. Что бы вы ни выбрали, для пользователя это должно смотреться единым приложением. Объединять можно как на этапе сборки, так и динамически – во время исполнения кода на стороне пользователя.
Таким образом все способы организации МФ-ов можно отнести к buildtime или runtime. У каждого свои плюсы и минусы.
Buildtime |
Runtime |
|
Проверка типов |
есть возможность |
нет такой возможности |
Версионирование |
есть возможность |
не имеет смысла |
Независимый деплой |
нет такой возможности |
есть возможность |
Проверка типов играет важную роль в современной разработке. Когда она ведется отдельными независимыми подкомандами, это становится необходимостью. Как гарантировать консистентность МФ-ов, что они точно используют и прокидывают данные в нужном формате и т.д.
Объединяя микрофронты во время сборки, вы не лишены возможности проверить типы. В случае с рантаймовским объединением придется писать интеграционные тесты, чтобы фронт неожиданно не «взорвался» на проде.
Версионирование и независимый деплой сильно противоречат друг другу:
Версионирование предполагает, что можно взять любую версию МФ другой команды. Это особенно актуально, когда нужно провести дополнительные работы по апгрейду зависимостей МФ-а от других. Каждая команда выбирает более удачное время для апгрейда.
Независимый деплой дает больше автономности и независимости командам. Тут важно всегда использовать последние версии МФ-ов. Это обязывает соблюдать обратную совместимость.
Версионирование можно реализовать и при runtime-объединении, но это нецелесообразно. Связываться с рантаймом имеет смысл только ради независимого деплоя, а последний не может существовать вместе с версионированием.
Далее мы увидим примеры конкретных реализаций каждого подхода объединения МФ-ов.
Способы организации микрофронтенда: особенности, преимущества и недостатки
Iframe
Самый древний способ организации микрофронтов. iframe – это специальный тэг для передачи адреса ресурса, который будет отображен на основном сайте. В итоге получается сайт внутри сайта.
Из достоинств стоит отметить легкость реализации, полную изоляцию логики и стилей, возможность делать независимый деплой и отсутствие привязки к фреймворкам.
За эти положительные качества придется заплатить производительностью, поскольку каждая вставка iframe приводит к загрузке ресурсов. Вы не сможете избежать этого: попытки детачить и реатачить DOM-узел не сохранят ранее загруженные ресурсы, придется скачивать их заново. Минимизировать снижение производительности можно путем настройки кэширования. Для этого нужно настроить инвалидацию кэша по времени для иммутабельных ресурсов. К счастью, все современные cli из коробки для собранных js и css файлов крепят небольшой хэш в название. К недостаткам этого способа также отнесем неспособность поисковых роботов рендерить iframe-ы для последующей индексации.
Преимущества |
Недостатки |
Легкость реализации |
Производительность |
Изоляция логики и стилей |
SEO |
Независимый деплой |
|
Отсутствие привязки к фреймворку |
Оправданный сценарий применения этого способа – единичная вставка небольшого куска стороннего приложения, написанного на другом стеке, когда нет необходимости в SEO.
WebComponents
Создания нативных компонентов фронтенд-сообщество ждало давно, но в итоге они так и не завоевали массовую популярность, на которую многие рассчитывали. Тройка самых популярных фронтенд-фреймворков (React, Vue, Angular) до сих пор создают компоненты по-своему.
Несмотря на возможность создавать МФ-ы на веб-компонентах, на практике я такое не встречал. И это неспроста, тут есть ряд блокеров:
Либы вроде Lit или Stencil недостаточно популярны и распространены. К тому же, на рынке недостаточно специалистов, умеющих работать с ними или готовых научиться.
Angular-elements или vue-custom-element остаются экзотикой. В родной среде нет особого смысла их применять. Если уж дробить приложение, то на обычные npm-пакеты, чтобы потом можно было подключить компоненты как либы. Использовать веб-компоненты с другими фреймворками неоправданно, поскольку вместе со сгенеренными компонентами нужно подключать мини-версию фреймворка, на котором они были написаны.
Выносить сложные куски функционала в веб-компоненты может быть затратно. Поскольку нужно будет настраивать коммуникацию вашего компонента с остальным приложением, выносить целую страницу в отдельный кастомный компонент может быть неоправданно.
Поисковые роботы не могут создать веб-компонент, и это скажется на seo-оптимизации.
Преимущества |
Недостатки |
Подходит для сквозной функциональности |
Сложность реализации |
Сочетается с любым фреймворком |
SEO |
Изоляция логики и стилей |
В итоге остается только один достойный кейс применения веб-компонентов в МФ-ах: создание корпоративного UI-kit, который можно было бы применить на всех проектах. При этом неважно, на каком фреймворке написано, поскольку логика и стили изолированы. Использование контролов UI-kit на веб-компонентах не скажется на SEO, поскольку сами контролы не нужны для индексации страниц.
Примечание
В качестве механизма доставки веб-компонент можно использовать как npm, так и просто деплой на известный url, откуда потом забирать через тег script[src]. В случае npm повышается удобство использования при шаринге между проектами, но теряется независимый деплой. В случае обычного деплоя статики придется выстраивать порядок работы скриптов, а перед использованием кастомных элементов нужно убедиться в их регистрации – например, через механизм CustomElementRegistry.whenDefined()
NPM
Разработка через npm-пакеты имеет много преимуществ. Разработчики просто импортируют нужные им компоненты и файлы из либы. При этом в проекте сохраняется типизация, есть версионирование. Сборка оптимальна: работает tree-shaking (удаление неиспользуемого кода при сборке), а разработчики могут легко настроить lazy loading.
Но у этого способа есть и свои недостатки. В этом случае вы будете вынуждены выдерживать единство стека, а также заниматься поддержкой версий ваших МФ-ов и их транзитивных зависимостей. С другой стороны, это может быть и плюсом: опубликовав новую версию пакета, другие команды могут в удобное для них время брать в работу поднятие версии своих зависимостей, если требуется внедрить дополнительный функционал либо, наоборот, что-то убрать.
Преимущества |
Недостатки |
Производительность |
Нет независимого деплоя |
SEO |
Единый стек |
Проверка типов (при наличии TS) |
|
Версионирование |
Данный подход имеет смысл применять для конечного и точечного функционала, который можно легко выделить из проекта отдельно, а также для переиспользуемого функционала между проектами. Например, те же веб-компоненты можно упаковать в npm-пакет.
git submodules
В качестве альтернативы МФ на npm-пакетах рассмотрим сабмодули гита. По сути, это репозитории внутри репозитория, внутри которых также могут быть репозитории. В сабмодули можно выставить разные ветки. Например, у модулей фич можно поставить ветку-пустышку, в которой нет ничего. Это необходимо, чтобы сборка проходила быстрее и другие модули не создавали сайд эффектов. Заведение веток-пустышек может быть очень удобным при локальной разработке и тестировании.
По своим достоинствам и недостаткам данный подход очень близок к npm-пакетам. Мы также просто что-то импортируем, но уже из соседней папки, а не из пакета, и используем. Далее разберем отличия этих двух способов:
NPM-пакеты – это, по сути, конечный в меру изолированный микропродукт со своими релизами и версионированием. Все заточено на создание переиспользуемого функционала. Но приложение может быть сложным/запутанным и с «душком легаси», поэтому разбиение большого монолита на пакеты может быть весьма дорогостоящим. Именно тут будет разумным рассмотреть сабмодули, ведь они позволяют очень грубо нарезать репозиторий, когда мы без всякой дополнительной подготовки выносим папку в отдельный репозиторий.
NPM-пакеты могут быть вложены друг в друга рекурсивно. Сабмодули тоже, но на уровне сборки они могут начать дублировать функционал, если один из сабмодулей подключен несколько раз в разных папках как отдельный сабмодуль. В этом случае стоит использовать более плоскую структуру модулей.
Если нужно быстро выкатить фичу сразу во всех пакетах, вести кросспакетную разработку может быть крайне неудобно. В то время как на сабмодулях все остается единым целым, вы можете делать правки, которые затрагивают другие сабмодули. В этом случае легко заниматься отладкой. Но в итоге сами изменения вы не смержите – на уровне merge request сторонняя команда, чей модуль вы потрогали, может потребовать привести код в соответствие с их правилами.
npm |
git submodules |
Переиспользуемость |
Грубая нарезка функционала |
Произвольно вложенные зависимости |
Плоская структура |
Сложная кросспакетная разработка |
Разработка с любым количеством модулей |
Разрабатывать на сабмодулях не каждому понравится. Но это оправдано как альтернатива npm-пакетам в проектах с сильно связанной функциональностью.
nginx
Интересным и не совсем очевидным способом может быть разбиение вашего большого приложения на несколько маленьких, которые будут работать с конкретными роутами. После этого на уровне nginx можно указать, на каких роутах какой бандл отдавать. Это напоминает паттерн API-шлюз, который часто используется в классической микросервисной архитектуре.
Мини-приложение может отвечать за несколько роутов, переход между которыми реализуется средствами вашего SPA-фреймворка. Уход в другое мини-приложение выполняется через обычный a[href]. Очевидно, что перемещения между приложениями будут приводить к перезагрузке страниц.
Также неприятностей добавляет и использование общих компонент (хедер, футер, меню), логика и верстка которых должны быть продублированы в каждом приложении. Это можно минимизировать путем веб-компонент, но в целом это скажется на производительности. Однако данный подход обладает и преимуществами в виде изоляции логики со стилями и независимостью от фреймворка.
Преимущества |
Недостатки |
Легкость реализации |
Производительность |
Изоляция логики и стилей |
Дублирование кода |
Независимый деплой |
|
SEO |
|
Без привязки к фреймворку |
Данный подход идеален, когда нужно резко сменить стек приложения. Новый функционал делается в новом приложении. По мере переноса страниц и их перепроектировании на новый лад из старого приложения они удаляются.
Примечание
Вместо nginx может использоваться любой другой прокси-сервер (Apache, HAProxy и т.п.)
single-spa
single-spa – это, по сути, фреймворк, сочетающий в себе другие фреймворки. Невероятно мощная технология, за которой скрывается огромное количество нюансов, – тема для отдельной статьи.
Схема схожа с iframe, но загрузка МФ-а делается теперь через нативный import + importmap или через Systemjs, если нужны полифилы.
Коротко разберем достоинства и недостатки:
Преимущества |
Недостатки |
Независимый деплой |
Большая документация, которая все равно покрывает не все кейсы |
Без привязки к фреймворку |
Сложности c SEO (все рабочие демки только на React) |
Мощный CLI для поднятия разных МФ |
В отличие от всех способов организации МФ-ов этот сильно заточен под объединение под собой разных фреймворков. Но стоит предостеречь от использования технологии ради самой технологии. Если есть возможность обойтись одним стеком, нужно этим пользоваться. Разработка может быть навсегда обременена поддержкой технически сложного проекта и правкой каких-либо багов от сайд эффектов разных приложений. Пользователь же может ощущать дискомфорт, т.к. количество кода для скачивания на клиент будет увеличено (ядра разных фреймворков для разных кусков функционала + ядро самого single-spa и его плагинов).
Этот подход подойдет при объединении разных фреймворков.
Примечание. Использование import maps делает разработку очень гибкой. Но поддержка этой фичи на конец 2022 не внушает полного доверия.
Приходится использовать полифил на основе Systemjs, с которым не всегда совместима сборка фреймворков.
module federation
Плагин webpack 5, который разрабатывался специально для создания МФ-ов. Перспективная технология: небольшой плагин для сборки для более корректного бандлинга и динамических импортов в рантайме.
Схема почти один в один повторяет single-spa, но теперь для подгрузки МФ-ов применяются динамические импорты
Соответственно, топ 3 cli (create-react-app, vue-cli, angular-cli) в «ближайших» релизах могут обзавестись поддержкой этого плагина и нужными консольными командами.
Преимущества |
Недостатки |
Независимый деплой |
Низкоуровневость |
Легкость реализации (демки от создателей под разные фреймворки) |
|
Нет противоречий с SSR |
Если есть возможности и желание поддерживать единый стек, а также необходимость дробления на микрофронтенды, то в 2022 году этот способ является одним из самых достойных решений. Стоит лишь отметить, что плагин в меру примитивный, он лишь делает правильную сборку. В случае недоступности МФ-а, нужно озадачиться правильной обработкой исключений.
Что выбрать для проекта?
Давайте еще раз пробежимся, что и для чего можно применить:
iframe – одиночная вставка для сочетания несочетаемого
web components – когда нужна небольшая сквозная функциональность без привязки к фреймворку, вроде корпоративного ui-kit
npm-пакеты – если есть переиспользуемость между проектами и/или нужна проверка типов в билд тайме
git submodules – когда нужно грубо нарезать проект и распределить зоны ответственности
nginx – если хотите поскорее сменить фреймворк на проекте, желательно не застрять на этом решении, ведь «нет ничего более постоянного, чем временное»
single-spa – когда есть сильная необходимость сочетать несколько фреймворков неопределенно долгое время, желательно без SSR
module-federation – все остальные сценарии применения МФ-ов при условии единства стека.
Каждый подход хорош по-своему и всему должно быть свое место. Перед тем как переходить на МФ-ы, советуем задуматься, точно ли вам это нужно. Какой подход бы не был выбран он неизбежно что-то усложнит на уровне разработки, CI/CD или производительности. Если есть возможности оставаться на едином стеке и монолитном приложение, с радостью принимайте такую возможность.
И, безусловно, не забывайте о пользователях. В конечном итоге им скачивать все подключенные фреймворки и терпеть возможные баги от некорректной интеграции МФ-ов в разных кусках функционала. Бизнесу же, в свою очередь, придется платить за внедрение и поддержку всего этого.
А как вы относитесь к микрофронтендам? Используете ли в своей работе? Будет интересно узнать
Примечание
Если интересно почитать и другие наши материалы об архитектуре проектов, ссылки ниже:
https://habr.com/ru/company/simbirsoft/blog/512310/
Больше кейсов и полезных материалов в наших каналах:
Комментарии (11)
nin-jin
10.11.2022 10:28А какая разница независимый деплой или централизованный, но автоматизированный?
SSul
10.11.2022 13:16Автоматизированный подход может помочь взять лучшее от этих двух подходов: простоту типизации и независимость команд, но надо учесть массу нюансов.
Разберем npm пакеты. В package.json можно указать диапазон версий, что даст возможность апать пакет и триггерить сборку главного приложения. Но в package-lock.json зафиксирована конкретная версия пакета, которая работала на момент ручной интеграции или обновления пакета разработчиком. Опасно удалять этот файл и ставить зависимости заново, так как могут быть зарезолвлены иные версии сторонних зависимостей. Можно точечно обновить пакет и сделать комит через CI, чтобы разработчики могли отребейзиться и увидеть изменения в зависимостях, но в этом случае большая ответственность возлагается на DevOPS-инженеров.
С сабмодулями Git проще, поэтому это решение выглядит рабочим. Можно сделать git submodule foreach "git checkout master", а затем дергать сборку, но здесь может быть появиться блок со стороны разработки. Не всем нравится разработка через сабмодули, так как она приводит к сильной связности кода и проект ощущается монолитом. Когда проект достаточно большой, то хочется большей изоляции фич друг от друга.
noodles
12.11.2022 14:02внедрение МФ-ов может дать более управляемую разработку.
Ключевое "может". Административные методы (раскидывание зон\задач между командами) не дешевле и не надёжние ли будут чем организация и поддержка "микрофронтендов" на долгосроке?
Как организовать взаимоотношения между таким количеством людей? Как выстроить процессы на проекте такого масштаба? Как грамотно разграничить зоны ответственности?
Ну как как.. общением.. собрал лидов команд - вы делайте это, а вы - вот это, и не мешайте друг-другу. Если появляется пересекающиеся проблемы - опять собираемся и проговариваем как будем делать. Ну да, интровертам нужно выходить из зоны комфорта чаще)) зато меньше абстракций\кода - выше надёжность.. разве не так?
NGINX
разбиение вашего большого приложения на несколько маленьких, которые будут работать с конкретными роутами.По ощущениям - единственный православно правильный способ. Делали так ещё пять лет назад - но тогда не было терминов "микрофронтенды"..))
mayorovp
Я бы поспорил с утверждением, что при компоновке приложения в рантайме нет возможности проверить типы.
Можно же интерфейс подгружаемого модуля описать в пакете, и проверять типы при сборке каждого микрофронтенда. А с устаревшими версиями или мусором можно бороться указывая версию контракта в свойстве с именем, которое случайно не возникнет.
Ну вот как-то примерно так:
nin-jin
Тут проблема не в самой проверке, а в том, что на проде может быть не та версия, с типами которой ты проверялся. Тут разве что типы с прода брать.
mayorovp
Для этого и нужна my_appshell_contract_version.
Да, совместимость типов между версиями придётся поддерживать вручную. При сильном желании и тут можно что-нибудь придумать, но лучше всего просто как можно реже менять api.
SSul
Уточните, что имелось в виду:
В рантайме мы можем типизировать с помощью утиной типизации. Не очень удобно, громоздко, для декларативного подхода нужно json валидаторы писать или использовать либы. Но по сути все кастомные тайп гарды так и работают. Опять же ошибки всплывут в рантайме, просто мы сможем их качественней обработать
Можно попробовать составить d.ts файлы на основе сборки МФ-ов. Файлы эти нпм пакетами доставлять в основное приложение. Именно этот вариант вы и описали, как я понял. Но тогда надо будет при каждом деплое МФ-а пересобрать основное приложение с учетом потенциально изменившихся типов. При этом версии этих пакетов надо менять, ведь они зафиксированы в package-lock. Апать пакет и комить новый резолв зависимостей через ci, чтоб разработчики могли потом подтянуть изменения и узнать, что да как там в новой версии. Способ компромиссный, но по-своему хлопотный
mayorovp
Нет. Я предлагаю выделить отдельный пакет, описывающий api между оболочной и микрофронтами, и подключать его всюду. А внутрь этого пакета встроить механизм контроля версий, позволяющий проверить совместимость с другой копией того же пакета.
SSul
По мнению автора:
Заниматься поддержкой версий при независимом деплое идеологически неверно. Должна быть обратная совместимость, раз мы пошли этой дорогой. И это не то же самое, что проверка типа. Версия может поменяться, но типы остаться прежними. Как разработчикам AppShell узнать сигнатуры методов, чтоб локально все компилилось, если объединяемся в рантайме? Только чтение документации к МФ-ам, а тут могут быть и человеческие ошибки. Допустим мы компилим под флаг strictNullChecks, передаем null в метод, который оказывается не умеет работать с null. Без тайпингов компиляция не отловит ошибки. Поэтому и предложен "замороченный" вариант с одновременной сборкой МФ-а и сборкой тайпингов под пакет
mayorovp
А нахера разработчикам AppShell знать сигнатуры методов микрофронтендов? Это разработчики микрофронтендов должны знать сигнатуры api AppShell. В конце концов, неработающий микрофронтенд — проблема автора микрофронтенда.
И самый простой способ обратной совместимости достичь — как можно реже менять api, либо менять его обратно-совместимым способом. Но можно и что-то вроде семантического версионирования устроить: