На самом деле инструментов на текущий момент очень много и каждый инструмент идеален по своему. Но чаще всего мой выбора падает на module federation 2.0 по нескольким причинам:
-
Генерация типов
-
Обновления типов в реальном времени (type hot reload)
-
Глубокая интеграция с webpack. Все же при выборе сборщика я часто собираю на нем.
А теперь когда нам стали ясны все плюсы, предлагаю начать с основного и посмотреть на наш базовый конфиг.
type ModuleFederationOptions = {
name: string; // Имя для module federation
filename?: string; // Имя для remoteEntry
remotes?: Array<RemoteInfo>; // Удаленные объекты, которые будет использовать приложение
exposes?: PluginExposesOptions; // Это файлы, которые это приложение будет предоставлять как удаленные объекты другим приложениям.
shared?: ShareInfos; // Конфигурация зависимостей
dts?: boolean | PluginDtsOptions; // Контроль типов
}; Чтобы легче было понять что за что отвечает , давайте представим что у нас день рождения, а наш конфиг - это наша карточка которая подскажет гостям кто мы такие.
-
Тогда получается что name - это ваше имя.
Зачем нужно: Чтобы другие могли тебя найти и знали как к тебе обращаться. -
fileName - Имя файла, который ты раздал гостям, чтобы все знали что ты умеешь делать.
Зачем нужно: Чтобы другие могли найти тебя и узнать что ты можешь делать и чем помочь. -
remotes - Список друзей, которых ты позвал на свой праздник.
Зачем нужно: Чтобы найти приглашенных друзей и знать, кто тебе будет помогать и что они умеют. -
exposes - что ты сам умеешь делать и что можешь предложить другим.
Зачем нужно: Чтобы другие знали чем ты можешь помочь. -
shared: Общие вещи которые вы с друзьями будете использовать вместе.
Зачем нужно: Чтобы все пользовались чем то одним и не возникло конфликтов из за использования разных вещей, например как использовать на дне рождения все одинаковые тарелки и никого не обидеть. -
dts: инструкция.
Зачем нужно: как работать друг с другом и не допускать ошибок.
Давайте чуть более подробно поговорим о настройках shared. Вернемся к примеру с вечеринкой. Как мы помним shared - вещи которые вы с друзьями будете использовать вместе. Например собирать конструктор.
-
singleton - в примере конструктора это как вы договорились использовать одинаковые детальки. Т.е. в техническом плане, наш модуль будет загружаться только один раз.
Возможные проблемы: Если указать false, то мы столкнемся с проблемой что если у нас в одном приложении версия react 18, в другом 17 и еще в одном 16, то тогда будут загружены все три версии. Это влечет за собой большие проблемы, например: увеличение размера приложения(станет тяжелым, будет дольше грузится), конфликты совместимости, сложность в отладке(какая версия используется).
Зачем нужно: Чтобы все использовали одни кубики, которые стыкуются друг с другом, иначе все может сломаться(в определенных случаях, так как сейчас мы говорим о том что используем одинаковые версии). -
eager - сразу выкладываем все части конструктора на стол, что бы мы понимали что будем использовать. Т.е. загружаем модуль сразу после инициализации а не по требованию.
Зачем нужно: Зачем тратить время на поиск деталей, если они все будут на столе перед нами. -
requiredVersion: Версия деталей.
Зачем нужно: Чтобы все использовали одинаковые детальки.
А теперь когда мы знаем как зачем и что нужно, то после настройки нашего конфига мы можем описать нашу точку входа. Для базового использования микрофронтов, достаточно лишь немного видоизменить входную точку. Раньше наша точка входа (index.tsx) могла выглядеть так:
// index.tsx
import BusinessСard from '@exposes/BusinessСard/BusinessСard';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<BusinessСard value='Карточка' />); Сейчас же точка входа разбивается на 2 файла.
Это файл index.ts и bootstrap.tsx
// index.ts
void import('./bootstrap');
export {};// bootstrap.tsx
const BusinessСard = lazy(() => import('firstApp/BusinessСard'));
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Suspense fallback={<Loader />}>
<BusinessСard value='Карточка' />
</Suspense>,
);
Но тут же возникают вопросы:
-
Зачем мы разбиваем входную точку index.ts на два файла?
Основная задача файла index.ts (точки входа) — загрузить и выполнить код из bootstrap.tsx. Наш index.ts теперь загружается асинхронно, что очень важно: на момент загрузки у нас ещё нет информации о том, какие версии могут быть предоставлены другими микрофронтами. Все эти зависимости будут вынесены в отдельные чанки и загружены позже. (bootstrap нужен только для тех, кто экспортирует модули) -
Зачем нужен динамический импорт?
Он создаёт отдельный чанк для загружаемого модуля и помогает загрузить его только тогда, когда это действительно необходимо. -
Также прошу обратить внимание на <Suspense>
Этот компонент позволяет управлять состоянием загрузки асинхронных компонентов. Часто встречается подход, когда в <Suspense> оборачивают всё приложение, и на этом его использование заканчивается. Однако лучше оборачивать каждый компонент отдельно — это предотвратит "моргание" интерфейса и не превратит всю страницу в скелетон.
И так, теперь когда нам ясна основанная техническая часть, давайте дополним это теорией. Ниже вы увидите диаграмму от Тобиаса Коперса - создателя webpack, которая показывает как все элементы системы работают.
У нас есть хост, мы открываем страничку с основного сайта и
-
Браузер по скрипту начинает загружать наше приложение.
-
Далее подключаем remoteEntry, то есть происходит загрузка и извлечение бандла.Стоит отметить что скрипты могут загружаться параллельно, но при этом каждое приложение знает кто станет хостом а кто ремоутом.
-
Как только загрузился хост, он начинает загружать приложение, запуск меняется с синхронного на асинхронный.
-
Приложение загружается и уже понимает что в нем есть некий удаленный компонент, например в нашем случае бизнес карточка, которая грузится из какого то scope.
4.1 Происходит проверка версий, для того чтобы модуль реакта понимал какую версию ему необходимо взять. (не забываем про флаг сингл тон).
4.2 Приходит запрос получить карточку.
-
Далее инициализируется скоуп, куда передается версия реакта.
5.1 ) Скоуп загружает модуль с remote. При этом для ремоут батона проверяется стоит ли грузить все заново или подгрузить какие то чанки отдельно, например стили , шрифты и т.д.
-
После чего, если ошибок нет, то модуль возвращает нам асинхронный модуль бизнес карточки.
Давайте вкратце посмотрим на реальном примере, как всё это работает. Запускаем приложение и открываем DevTools.
Мы видим, как сначала загружается наше приложение: браузер по скрипту запустил основное приложение, после чего загрузился наш микрофронт. В сетевых запросах мы видим чанки основного приложения, а затем - наш remoteEntry.js (файл, который содержит информацию о модулях, предоставляемых микрофронтендом).
Далее мы просто берём нашу бизнес-карточку, и если всё хорошо, то получаем её. И всё! На самом деле, ничего сложного здесь нет. На текущий момент большинство ошибок, с которыми вы столкнётесь, хорошо описаны в документации.
Единственное, на что я хотел бы обратить особое внимание — обязательно проверяйте латиницу в путях вашего приложения. К сожалению, об этой ошибке мало где упоминается, и мне пришлось потратить немало времени, чтобы понять, почему моё приложение запускается, но типы не генерируются.
К слову о генерации: я предлагаю также на простом примере посмотреть, как работает генерация типов и обработка ошибок.
Итак, мы выводим нашу карточку