
Привет! Меня зовут Максим, я фронтенд-разработчик компании Тинькофф, лид команды фронтендов, которые пилят международные проекты. Я работал как фронтом, так и бэкером — это дало мне релевантный опыт и в микрофронтендах в том числе.
Статья будет о фронтендах, но сначала предлагаю немного обсудить монолиты. Они бывают разные.
Зачем пилить монолит
Когда есть команда, поддерживающая один большой продукт, и этот продукт — монолит, можно сказать, что ей повезло. Не нужно париться с микрофронтендами, хорошая разработка закрывает все вопросы. Бизнес — доволен, заказчики — довольны.
Проблемы с монолитами появляются, когда в разработке одного продукта участвуют две и больше команд. Начинаются конфликты, портятся отношения в команде.
Разберем пример. Возьмем условное приложение — любой городской портал. Там есть новости, продажа или аренда жилья и статистика по автомобилям.

В приложении появилось несколько десятков конфликтов, образовалась очередь на релизы. Это больно и трудно.
В один прекрасный момент руководитель команды принял решение распилиться. Но помимо монолитного фронта обычно есть еще и монолитный бэк. А это значит, что будем распиливать и фронт, и бэк. Чтобы все прошло успешно, нужен план:
- Собраться всеми командами, которые участвуют в разработке продукта. 
- Определить, что, как и зачем будем пилить. 
- Придумать схему нового решения. 
- Внести правки в новое решение. 
- Начать распил. 
- Забыть про отказоустойчивость решения. 
На выходе получится примерно такая схема:

Основная проблема монолита — это бэк, когда много всего намешано в одной базе и бизнес-логика написана разными методами. Проблемой фронта это стало относительно недавно. Поэтому часто при распиле на микросервисы забывают фронт. Но фронт нам все равно нужно тоже распиливать, и решений, как распилить фронт, много.
Можно оставить все как есть, если текущий 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)
 - imater24.11.2022 17:22- 1) А микрофронтенд может использовать другой микрофронтенд? Или всех их разруливает (роутит) ядро? 
 2) Работает ли hot reload при разработке?
 3) Работает ли SSR? И видит ли node.js что микрофронтенд обновился при hot reload - FindYourDream Автор24.11.2022 18:48- 1. Да можно использовать, для этого внутри ремоута нужно определить роут и вызвать с помощью функции loadRemoteEntryModule(). 
 2. При использовании nx-workspace автоматом перезагружается хост, но появилось это относительно недавно, до этого приходилось ручками обновлять страницу в браузере. В остальном hot работает как надо
 3. Поддержка SSR заявлена, но не совсем отлично работает, поэтому данный момент пока держу за скобками и отложил в бэклог) - icherniakov25.11.2022 13:34- Спасибо за статью, как раз сейчас стоит задача перевести приложение с микрофронтами (angular-architects) на SSR. Отсюда просьба, дать какой-то совет или ссылку "что почитать", чтобы этот путь не был столь долог, опасен и не стал тупиковым) пожалуйста!  - markelov6925.11.2022 14:04- puppeteer + cache, в фоне кэш страниц обновляется, а на запрос странички отдается сразу готовый HTML из кэша, работает супер быстро и никаких переделок на фронте не требует, после загрузки бандла просто данные зафечатся по новой которые при загрузки отображаюся и пользователь увидит их самыми актуальными, а для поисков пофиг что они могут быть на 5-10 минут "устаревшие", на SEO это не влияет  - icherniakov25.11.2022 14:32- Оу, спасибо, изучу и этот вариант!  - mayorovp26.11.2022 08:19- Осторожнее с этим вариантом. - Запуск браузера на сервере для получения сгенерированного фронтом HTML — это куча накладных расходов, которых, как правило, можно избежать.  - markelov6926.11.2022 11:59- Нет. Это копейки. 1-2 экземпляра запущенных которые обновляют кэш и периодически перезапускаются дабы избежать возможных утечек памяти. 
 Альтернативы 3:
 1) Нe использовать SSR.
 2) Использовать классические схемы типо PHP + jQuery и т.п.
 3) Превратить свой проект в говнокод и получить на ровном месте дополнительно связанные руки, ограничения, реальную нагрузку на сервер и использовать всяческие Next.js, Nuxt.js и прочую фигню.
 
 
 
 
 
 
 - radtie24.11.2022 19:28- Микрофронтенды решают много проблем, но и привносят достаточно новых.  - FindYourDream Автор25.11.2022 10:04- Как и любая другая технология в целом. Серебряных пуль не существует. 
 
 - Tzar4eg25.11.2022 10:04- Я так понимаю используется несколько ангуляр приложений. Не возникает проблема с zone.js?  - FindYourDream Автор25.11.2022 10:05- Нет, проблем не возникает. На уровне конфига мы можем управлять зависимостями и синглтонить их. Поэтому все что касается ангуляра автоматом прописано как singlton: true 
 
 - Femistoklov25.11.2022 10:30- Но помимо монолитного фронта обычно есть еще и монолитный бэк. А это значит, что будем распиливать и фронт, и бэк. - Ловко. Возьму на вооружение. - - Надо отрефакторить фронт. - - Значит и бэк будем, а то что это он! 
 - Lonli-Lokli26.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 версии, можно рассмотреть данное решение.