Антон Ларичев

Введение
Монолитный фронтенд — узкое место крупных команд. Когда над одним SPA работают десятки разработчиков, слияния становятся болью, деплои затяжными, а любое изменение рискует сломать всё приложение. Микрофронтенды решают эту проблему, разбивая UI на независимые части, каждую из которых можно разрабатывать, тестировать и деплоить отдельно.
В Webpack 5 появился Module Federation — нативный механизм для загрузки кода из других сборок прямо в рантайме. Это сделало микрофронтенды доступными без сложной инфраструктуры и сторонних фреймворков.
Архитектурные подходы
Перед тем как переходить к коду, важно понять, какие стратегии существуют.
Iframe-подход
Самый простой способ: каждый микрофронтенд живёт в <iframe>. Полная изоляция по стилям и JavaScript, но слабая коммуникация между частями и плохой UX из-за отдельных скроллов и истории браузера.
Независимые SPA на разных маршрутах
Каждое приложение отвечает за свой URL-префикс. Shell-приложение (оркестратор) подгружает нужный SPA при переходе по маршруту. Подход хорош для крупных доменов с минимальным пересечением UI.
Компонентная интеграция через Module Federation
Наиболее гибкий подход: приложения делятся компонентами в рантайме. Shell монтирует компоненты из удалённых сборок как обычные React-компоненты. Именно этот способ разберём подробно.
Module Federation: как это работает
Module Federation — плагин Webpack 5, позволяющий одной сборке динамически загружать модули из другой во время выполнения программы.
В схеме два типа участников:
- Host (оболочка) — основное приложение, которое потребляет удалённые модули
- Remote (поставщик) — приложение, которое экспортирует свои модули
Настройка Remote-приложения
// webpack.config.js удалённого приложения (каталог товаров)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
// уникальное имя контейнера
name: 'catalog',
// файл точки входа федерации
filename: 'remoteEntry.js',
exposes: {
// псевдоним для импорта → реальный путь к файлу
'./ProductList': './src/components/ProductList',
'./CartButton': './src/components/CartButton',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Параметр shared критически важен: он гарантирует, что все приложения используют одну копию React, а не тянут каждое свою.
Настройка Host-приложения
// webpack.config.js хост-приложения (shell)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// псевдоним → имя контейнера@URL до remoteEntry.js
catalog: 'catalog@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Динамическая загрузка компонентов в React
Удалённые модули загружаются асинхронно, поэтому используем React.lazy и Suspense:
// src/App.jsx в host-приложении
import React, { Suspense } from 'react';
// ленивая загрузка компонента из удалённого приложения
const ProductList = React.lazy(() => import('catalog/ProductList'));
const CartButton = React.lazy(() => import('catalog/CartButton'));
function App() {
return (
<div className="shell">
<header>
<Suspense fallback={<span>Загрузка корзины...</span>}>
<CartButton />
</Suspense>
</header>
<main>
<Suspense fallback={<span>Загрузка каталога...</span>}>
<ProductList />
</Suspense>
</main>
</div>
);
}
Коммуникация между микрофронтендами
Компоненты из разных сборок не могут напрямую пользоваться одним React Context. Для обмена данными подходят три паттерна.
Пропсы через Host
Самый простой способ — Host передаёт данные в удалённый компонент через пропсы:
// Host знает текущего пользователя и пробрасывает это в каталог
<ProductList userId={currentUser.id} onAddToCart={handleAddToCart} />
Общая шина событий
Для слабосвязанной коммуникации подходит простой pub/sub, вынесенный в shared-библиотеку:
// packages/event-bus/index.js — общая для всех сборок
const listeners = {};
export const eventBus = {
emit(event, data) {
(listeners[event] || []).forEach(fn => fn(data));
},
on(event, fn) {
listeners[event] = [...(listeners[event] || []), fn];
// возвращаем функцию для отписки
return () => {
listeners[event] = listeners[event].filter(l => l !== fn);
};
},
};
URL как источник истины
Состояние, зашитое в URL (query-параметры, pathname), автоматически доступно всем микрофронтендам без дополнительной инфраструктуры.
Частые ошибки
Дублирование зависимостей
Если не указать shared или указать конфликтующие версии, каждое приложение загрузит свою копию React. Это приводит к ошибке «Invalid hook call» и росту бандла. Всегда явно декларируйте shared-зависимости с singleton: true.
Синхронный импорт на верхнем уровне
Module Federation загружает удалённые модули асинхронно. Статический import в точке входа без асинхронной обёртки ломает сборку. Правильное решение:
// index.js — точка входа должна быть асинхронной
// bootstrap.jsx содержит ReactDOM.createRoot и рендер приложения
import('./bootstrap');
Хардкод URL удалённых сборок
http://localhost:3001 подходит только локально. В продакшене URL задаётся через переменные окружения или динамическую конфигурацию, иначе придётся пересобирать Shell при каждом изменении адреса Remote.
Отсутствие Error Boundary
Если удалённый сервис упал, весь Shell сломается вместе с ним. Оборачивайте каждый Remote в Error Boundary:
// возвращает заглушку, если удалённый компонент не загрузился
class RemoteBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
render() {
if (this.state.error) {
return <div>Раздел временно недоступен</div>;
}
return this.props.children;
}
}
Заключение
Module Federation убирает главный барьер микрофронтендов — сложность инфраструктуры. Компоненты из разных команд монтируются как обычные React-элементы, при этом каждая команда деплоит независимо и в своём темпе.
Ключевые принципы успешной реализации: явные shared-зависимости с singleton: true, асинхронные точки входа, Error Boundary вокруг каждого Remote и конфигурация URL через переменные окружения. Начните с одного Remote на маршрут — это наименее рискованный старт — и переходите к компонентной интеграции по мере роста команды.






Комментарии
0