Всем привет! Меня зовут Игорь. Я frontend-разработчик. Сегодня я расскажу вам, насколько просто использовать микрофронты. Причина, по которой я хотел бы рассказать об этом подробнее, в том, что люди, которые недавно в разработке, не всегда представляют, как приложения взаимодействуют между собой и через что приходится пройти нашему remote-модулю, чтобы он смог отобразиться на хосте.
Перед началом предлагаю немного остановиться на терминологии и стеке:
-
Микрофронтенд — это независимый модуль, который мы подключаем.
-
Хост — это бандл, куда мы подключаем микрофронтенд.
-
Remote — это бандл, который мы импортируем.
Мы будем использовать следующий стек технологий: React, typescript, webpack, webpack mf 2.0.

Микрофронтенды — это архитектурный подход к разработке фронтенд-приложений, который подразумевает разбиение пользовательского интерфейса на независимые, самодостаточные модули. Предлагаю пропустить плюсы и минусы данного подхода, так как о них уже довольно много написано, и перейти сразу к преимуществам выбора нашего основного инструмента, благодаря которому мы будем реализовывать этот подход.

На самом деле инструментов на текущий момент очень много и каждый инструмент идеален по своему. Но чаще всего мой выбора падает на 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 (файл, который содержит информацию о модулях, предоставляемых микрофронтендом).
Далее мы просто берём нашу бизнес-карточку, и если всё хорошо, то получаем её. И всё! На самом деле, ничего сложного здесь нет. На текущий момент большинство ошибок, с которыми вы столкнётесь, хорошо описаны в документации.
Единственное, на что я хотел бы обратить особое внимание — обязательно проверяйте латиницу в путях вашего приложения. К сожалению, об этой ошибке мало где упоминается, и мне пришлось потратить немало времени, чтобы понять, почему моё приложение запускается, но типы не генерируются.
К слову о генерации: я предлагаю также на простом примере посмотреть, как работает генерация типов и обработка ошибок.
Начнем с обработки ошибок

Представим что наш микрофронтенд на загрузился, например сервер упал и файл remoteEntry.js недоступен. Логично что тогда упадет все приложение , куда был подключен наш микрофронтенд. Соответственно встает вопрос о том, что мы можем сделать , чтобы правильно обработать ошибку

У нас все получается.
На скриншоте изображён один из самых простых примеров. На самом деле можно также проверять доступность микрофронтенда перед загрузкой — например, отправлять запрос по адресу микрофронтенда. Кроме того, ошибки можно перехватывать с помощью обёртки ErrorBoundary, используя метод жизненного цикла componentDidCatch, который предназначен для обработки ошибок.
Сегодня мы не будем рассматривать эти варианты, но, думаю, вам самим будет интересно написать обёртку, подходящую под ваши конкретные задачи.
А пока давайте ещё раз рассмотрим наш текущий вариант обработки ошибок.
-
lazy — функция которая позволяет загружать компоненты динамически.
Зачем нужно: Чтобы загружать компоненты только тогда, когда они нужны, а не сразу при загрузке приложения. -
() => import, Import, он возвращает нам промис.
-
catch — обработка ошибок. Именно здесь мы можем обработать ошибку и например показать запасной Ui.
-
{default: () => void}: Если загрузка компонента не удалась, мы возвращаем запасной компонент. Здесь мы возвращаем объект с ключом default, который содержит функцию возвращающую react component. react lazy ожидает модуль с ключом default, default здесь показывает что наш модуль экспортирован по дефолту.
MF 2.0 автоматически генерирует типы
Настройки в dts используются для генерации типов в нашем приложении, которое мы хотим подключить к хосту (remote).Параметр consumesTypes необходим для использования ранее сгенерированных типов. Также хочу напомнить про документацию на официальном сайте — она может помочь, если возникнут какие-либо вопросы.
dts: { generateTypes: { extractRemoteTypes: true, extractThirdParty: true, deleteTypesFolder: true, generateAPITypes: true, compileInChildProcess: true, }, consumeTypes: { consumeAPITypes: true, deleteTypesFolder: true, maxRetries: 3, }, }
DTS:
-
deleteTypesFolder: true — Удалять ли созданную папку типа.
-
extractThirdParty: true — Когда содержимое exposes содержит модуль, содержащий antd, а у потребителя он неe установлен, то extractThirdParty: true можно гарантировать, что потребитель может нормально получить тип модуль exposes.
-
extractRemoteTypes: true — Когда контент производителя exposes имеет собственный, который реэкспортирует себя, то extractRemoteTypes: true можно гарантировать, что потребитель может нормально получить тип модуля производителя.exposes.
-
generateAPITypes: true — Генерировать ли loadRemoteтип в Federation Runtime.
-
compileInChildProcess: true — Выдавать ли ошибку при возникновении проблемы во время генерации типа.
consumesTypes:
-
consumeAPITypes: true — Генерировать ли тип loadRemoteAPI среды выполнения.
-
deleteTypesFolder: true — Перед загрузкой файлов типа «Удалять ли ранее загруженный types Folder каталог»
-
maxRetries: 3 — Максимальное количество повторных попыток при неудачной загрузке
Итак, как же происходит генерация типов? Удалённое приложение генерирует сжатый файл типа @mf-types.zip (это имя установлено по умолчанию). Генерация происходит во время сборки. После этого наш хост автоматически извлекает файл типов remotes и распаковывает его в @mf-types (это имя также установлено по умолчанию).
Мы запускаем наше remote приложение, у нас генерируется @mf-types.zip , Внутри него мы видим BusinessСard.d.ts, он экспортирует типы которые находятся в compiled- types и далее путь к нашему компоненту, который мы указывали в exposes.
Теперь если мы запустим наш Host , то мы увидим что у нас сгенерировалась папка @mf-types и если мы пройдем по пути, который проходили ранее для zip папки, то мы увидим здесь абсолютно идентичный интерфейс.
Важно: не забываем про базовые настройки вашего tsconfig.json. так как Remote приложение генерирует типы через tsc или esbuild, а хост приложение подхватывает их.
Сегодня мы прошлись по самым верхам и я надеюсь что вам стал понятен сам принцип работы микрофронтов. Ведь не так важно какие инструменты вы используете для реализации архитектуры микрофронтендов, важно понимать сам принцип а со всем остальным поможет документация. MF 2.0 делает микрофронтенды проще, безопаснее, быстрее и нагляднее.
ссылка на оригинал статьи https://habr.com/ru/articles/898278/
Добавить комментарий