Привет! Меня зовут Максим, я фронтенд-разработчик компании Тинькофф, лид команды фронтендов, которые пилят международные проекты. Я работал как фронтом, так и бэкером — это дало мне релевантный опыт и в микрофронтендах в том числе.
Статья будет о фронтендах, но сначала предлагаю немного обсудить монолиты. Они бывают разные.
Зачем пилить монолит
Когда есть команда, поддерживающая один большой продукт, и этот продукт — монолит, можно сказать, что ей повезло. Не нужно париться с микрофронтендами, хорошая разработка закрывает все вопросы. Бизнес — доволен, заказчики — довольны.
Проблемы с монолитами появляются, когда в разработке одного продукта участвуют две и больше команд. Начинаются конфликты, портятся отношения в команде.
Разберем пример. Возьмем условное приложение — любой городской портал. Там есть новости, продажа или аренда жилья и статистика по автомобилям.
В приложении появилось несколько десятков конфликтов, образовалась очередь на релизы. Это больно и трудно.
В один прекрасный момент руководитель команды принял решение распилиться. Но помимо монолитного фронта обычно есть еще и монолитный бэк. А это значит, что будем распиливать и фронт, и бэк. Чтобы все прошло успешно, нужен план:
Собраться всеми командами, которые участвуют в разработке продукта.
Определить, что, как и зачем будем пилить.
Придумать схему нового решения.
Внести правки в новое решение.
Начать распил.
Забыть про отказоустойчивость решения.
На выходе получится примерно такая схема:
Основная проблема монолита — это бэк, когда много всего намешано в одной базе и бизнес-логика написана разными методами. Проблемой фронта это стало относительно недавно. Поэтому часто при распиле на микросервисы забывают фронт. Но фронт нам все равно нужно тоже распиливать, и решений, как распилить фронт, много.
Можно оставить все как есть, если текущий UI всех устраивает. Главное — соблюдать основной постулат разработки: «Работает — не трогай». Но есть минус такого решения: неудобно тащить новую функциональность, разработчики будут страдать и начнется война за место в релизе. Чтобы все разрулить, появилась новая должность — Senior Conflict Manager. Человек, который будет собирать релизы.
Если текущий UI не подходит, будем распиливать его на микрофронты.
Что такое микрофронтенд
Название микрофронтенд появилось в 2016—2017 годах. Это некий постулат идей о том, как должно выглядеть приложение. Идей около 17, и на сайте Micro-Frontends.org они все расписаны.
Я выделил три важных аспекта микрофронта.
Изолированный код каждой команды. Не должно быть переплетений. Как этого добиться — вопрос двоякий. Можно уехать в другие репозитории, можно в разные NPM-скопы и прочее.
Уникальный префикс для каждой команды. Не должно быть пересечений, тогда не будет пересечений в коде.
Выбор нативного API, который предоставляет фреймворк. Это могут быть нативные фишки браузера, нативные фишки фреймворка, и не нужно писать свои костыли. Если что-то не нравится, можно заявить в issue на GitHub в этот фреймворк либо попробовать закатить пул-реквест.
Я убедился опытным путем, что не стоит придумывать свои решения. Когда я придумывал свои решения, мне казалось, что они классные и отлично работают. Но мои фронтендеры потом в конце рабочего дня плакали.
Какие есть варианты распила
Распил в NPM-библиотеке. У каждой команды будет своя страничка, все с отдельными тегами и хранятся в разных местах — определен свой NPM-скоуп, и код можно разнести. Понадобится приложение-синхронизатор, которое возьмет код всех команд и подключит к себе.
Микросервис все же не об этом. Он о том, что два приложения могут вместе работать и взаимодействовать между собой. А что, если сделать три приложения?
Использование монорепозиториев. Когда есть один репозиторий и создается несколько приложений, все они хранятся в своих отдельных папках и получаются локальные библиотеки. Как NPN-библиотека, только не в NPN, а локально в коде, и при сборке попадет в зависимость от каждого из приложений.
Нужно сделать библиотеку с хедером и завести три приложения. Каждая команда реализует свой блок логики, у них есть общий хедер, через который осуществляется навигация.
На выходе получается вроде бы монорепозиторий. Но нужен SPA. Решение нашлось в 2018 году, когда появился пятый Webpack. Помимо всяких ускорений работы он принес с собой Modul Federation.
Модуль имеет простую концепцию — притягивать через динамический импорт целое приложение. Будет условное приложение-хост, в котором описан загрузчик и указаны дочерние приложения для загрузки в основное.
plugins: [
new ModuleFederationPlugin({
remotes: {
main: 'main@http://localhost:4201/remoteEntry.js',
rent: 'rent@http://localhost:4202/remoteEntry.js',
cars: 'cars@http://localhost:4203/remoteEntry.js',
},
shared: share({
'@angular/core': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/common': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/common/http': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/router': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/forms': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
...sharedMappings.getDescriptors(),
}),
}),
sharedMappings.getPlugin(),
],
Есть два приложения, в рамках MFE это называется Host и Remote. У host-приложения есть webpack-конфиг с плагином MFE, он принимает в себя список зависимых приложений и библиотеки, которые должны быть едиными для всех приложений.
Подробнее про Remotes
Блок Remotes — это webpack-конфиг хост-приложения, приложения-синхронизатора. В этом блоке определяются названия проектов, они уникальны и должны иметь свой префикс.
Указывается URL-адрес, откуда забирать. Shared-секция описывает, какие зависимости в package.json должны иметь строгую версию и должна ли эта зависимость являться single-тоном.
Блок Remotes позволяет разнести свои приложения по разным доменам. В плане при распиле монолита был пункт «забыть про отказоустойчивость». Про нее забывают всегда.
Такое нововведение, которое позволяет с хоста загружать свой файл, дает развернуть приложение на отдельном железе или отдельном деплойменте в кубе. Чтобы все приложения стали независимыми и имели разное количество памяти, ЦПУ.
Если кому-то надо больше — накидываем больше. Например, пять разных приложений — пять разных деплойментов, но будет выглядеть как SPA.
В Angular есть файлик app.module.ts, там описаны компоненты, модули, зависимости приложения, декларируется роутинг и многое другое. В рамках remote-приложения на MFE сохраняется app.module.ts remote, но появляется новый файлик — remote-entry.module.ts. В нем описываются зависимости приложения, которые нужны в remote-режиме.
Получается две схемы деплоя: можно загрузиться как независимое приложение, стоящее на отдельном хосте, и зависимое приложение через RemoteEntryComponent.
Но есть нюанс. Если в app-модуле нужно объявить root-зависимости, то во втором случае нужно определить child-зависимость. Они должны быть дочерними, потому что root-зависимости будут описаны в app-модуле нашего хост-приложения.
plugins: [
new ModuleFederationPlugin({
name: "main",
fileName: "remoteEntry.js",
exposes : {
'./Module': 'apps/main/src/app/remote-entry/entry.module.ts'
},
shared: share({
'@angular/core': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/common': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/common/http': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/router': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
'@angular/forms': {singleton: true, strictVersion: true, requiredVersion: 'auto'},
...sharedMappings.getDescriptors(),
}),
}),
sharedMappings.getPlugin(),
],
Внутри webpack.config.js добавляем плагин MFE, в котором описаны базовые импорты, правила и многое другое.
Появилась секция name, где нужно указать название дочернего приложения, которое мы разрабатываем. А в секции exposes указываем ссылку на тот модуль, который будет упаковываться в файл remoteEntry.js.
Аналогично описывается shared-секция тех зависимостей, что должны быть синхронизированы. Мы синхронизируем Angular, можем синхронизировать любую библиотеку из NPM-скоупа и любую локальную библиотеку, если используем там монорепозиторий.
Подробнее про Host
Все описывается в app-модуле. Я декларирую, определяю рутовый роутинг, который будет базовым для всего и главным, определяю несколько путей, импортирую к себе лейзи-модуль. Этот модуль должен быть загружен не сразу, при старте приложения, а когда мы перейдем на условный путь. Для этого делаем ссылку на динамический модуль дочернего приложения.
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RouterModule.forRoot([
{
path: 'main',
loadChildren: () => import('main/Module').then(m => m.RemoteEntryModule)
},
{
path: 'rent',
loadChildren: () => import('rent/Module').then(m => m.RemoteEntryModule)
},
{
path: 'cars',
loadChildren: () => import('cars/Module').then(m => m.RemoteEntryModule)
}
])
],
bootstrap: [AppComponent]
})
export class AppModule {}
Все, можно считаться микрофронтендером!
Дальше я напишу примерно такой UI, в котором будет хедер, а внутри хедера — ссылки, ведущие нас по приложению.
<div>
<h3>My city portal</h3>
<header>
<a [routerLink]="'/main'">Main</a>
<a [routerLink]="'/rent'">Rent</a>
<a [routerLink]="'/cars'">Cars</a>
</header>
<div>
<router-outlet></router-outlet>
</div>
</div>
Ангулярщики уже понимают, роутер-аутлет — секция, куда будет вставляться контент зависимого приложения. Есть роутинг — мы определили его верхнюю часть, и роутер-аутлет — блок, в котором будет находиться дочерний роутинг.
Дальше начинается магия — происходит анимация. Я хожу по нашему приложению, при нажатии на LoginPage у меня загружается файлик, который загружает в приложение LoginPage, при нажатии на Origination загружается приложение Origination. В приложении Origination есть кнопка Go To Lazy — это дочерний роутинг зависимого приложения. Ничего не перезагружается, все работает как классическое SPA, то есть фактически монолит, но им не является.
MainJS попадает к нам с загрузкой приложения. Появляется динамический импорт, который берет из конфига указанный URL и идет на хост за нужным файликом. Потом начинается процесс загрузки того приложения внутри нашего.
Пятый выход Webpack дал нам очень много фишек, направленных на ускорение сборок, улучшение минификации и улучшенную работу с плагинами. И как дополнение — гибкая система управления микрофронтами, которая работает, и дополнительно ничего больше не нужно.
Работа из коробки с нативом и голым Webpack — там уже есть pluginFederation и можно делать микрофронты. Все популярные фреймворки имеют в комплекте MFE.
Выводы
Микрофронты позволяют решить проблему конфликтов приложений. В нашем примере с точки зрения пользователя мы создали монолит — один раз зашел и все сделал, а на деле все на микрофронтах.
Webpack выпуском MFE понизил планку входа в микрофронты. Если раньше нужно было писать загрузчик, понимать, как все работает на деле, то теперь все стало проще — плагин, документация к нему. Идешь по документации и делаешь, как там написано.
Получается довольно гибкая работа с надежностью. Если в одном из условных пяти приложений команда что-то сломала, то есть health-чеки, проверяющие доступность приложений, и если приложение недоступно — вешают плашку. Можно с каждым приложением работать отдельно.
А в следующей статье расскажу, как мы съезжали на Modul Federation. Если есть вопросы — жду в комментариях.
Комментарии (20)
imater
24.11.2022 17:221) А микрофронтенд может использовать другой микрофронтенд? Или всех их разруливает (роутит) ядро?
2) Работает ли hot reload при разработке?
3) Работает ли SSR? И видит ли node.js что микрофронтенд обновился при hot reloadFindYourDream Автор
24.11.2022 18:481. Да можно использовать, для этого внутри ремоута нужно определить роут и вызвать с помощью функции loadRemoteEntryModule().
2. При использовании nx-workspace автоматом перезагружается хост, но появилось это относительно недавно, до этого приходилось ручками обновлять страницу в браузере. В остальном hot работает как надо
3. Поддержка SSR заявлена, но не совсем отлично работает, поэтому данный момент пока держу за скобками и отложил в бэклог)icherniakov
25.11.2022 13:34Спасибо за статью, как раз сейчас стоит задача перевести приложение с микрофронтами (angular-architects) на SSR. Отсюда просьба, дать какой-то совет или ссылку "что почитать", чтобы этот путь не был столь долог, опасен и не стал тупиковым) пожалуйста!
markelov69
25.11.2022 14:04puppeteer + cache, в фоне кэш страниц обновляется, а на запрос странички отдается сразу готовый HTML из кэша, работает супер быстро и никаких переделок на фронте не требует, после загрузки бандла просто данные зафечатся по новой которые при загрузки отображаюся и пользователь увидит их самыми актуальными, а для поисков пофиг что они могут быть на 5-10 минут "устаревшие", на SEO это не влияет
icherniakov
25.11.2022 14:32Оу, спасибо, изучу и этот вариант!
mayorovp
26.11.2022 08:19Осторожнее с этим вариантом.
Запуск браузера на сервере для получения сгенерированного фронтом HTML — это куча накладных расходов, которых, как правило, можно избежать.
markelov69
26.11.2022 11:59Нет. Это копейки. 1-2 экземпляра запущенных которые обновляют кэш и периодически перезапускаются дабы избежать возможных утечек памяти.
Альтернативы 3:
1) Нe использовать SSR.
2) Использовать классические схемы типо PHP + jQuery и т.п.
3) Превратить свой проект в говнокод и получить на ровном месте дополнительно связанные руки, ограничения, реальную нагрузку на сервер и использовать всяческие Next.js, Nuxt.js и прочую фигню.
radtie
24.11.2022 19:28Микрофронтенды решают много проблем, но и привносят достаточно новых.
FindYourDream Автор
25.11.2022 10:04Как и любая другая технология в целом. Серебряных пуль не существует.
Tzar4eg
25.11.2022 10:04Я так понимаю используется несколько ангуляр приложений. Не возникает проблема с zone.js?
FindYourDream Автор
25.11.2022 10:05Нет, проблем не возникает. На уровне конфига мы можем управлять зависимостями и синглтонить их. Поэтому все что касается ангуляра автоматом прописано как singlton: true
Femistoklov
25.11.2022 10:30Но помимо монолитного фронта обычно есть еще и монолитный бэк. А это значит, что будем распиливать и фронт, и бэк.
Ловко. Возьму на вооружение.
- Надо отрефакторить фронт.
- Значит и бэк будем, а то что это он!
Lonli-Lokli
26.11.2022 22:41Насколько понимаю, нельзя сделать шеллом реакт приложение, а вот ангуляр с его роутингом привязывать как микрофронт? То есть решение исключительно для ангуляр команд?
ПС почему Modul Federation? Вроде как Module Federation
andreyiq
А как решается проблема со стилями, что будет если в разных модулях окажутся стили для одинаковых классов?
FindYourDream Автор
Получение стилей как и остальной статики, разруливается через baseHref, так что мы исключили такую возможность. Модуль получит только тот набор стилей что запросил + глобальные стили из хоста
andreyiq
Не очень понял, что такое basehref? Для двух модулей на странице создадутся два элемента style в которых могут оказаться конфликты, например класс .content, который окажется в разных модулях в нескольких элементах, разве нет?
FindYourDream Автор
В данном кейсе главным выберется тот стиль, что прилетел из host приложения.
andreyiq
Почему из host? В хосте может вообще не быть таких стилей, либо приоритет окажется ниже того что в другом модуле. Получается один неудачный стиль может поломать отображение всего сайта
Dozalex
Css in js, например linaria, не используете до сих пор?
FindYourDream Автор
Для ангуляра не видится смысла в данном действии. С появлением нормального standalone API в 15 версии, можно рассмотреть данное решение.