В энтерпрайз-экосистемах с десятками распределенных продуктовых команд монолитные SPA-репозитории неизбежно становятся критическим узким местом: время сборки в CI/CD деградирует до десятков минут, релизные циклы блокируются взаимными зависимостями, а масштабирование кодовой базы приводит к постоянным конфликтам слияния в Git.
Архитектура микрофронтендов решает задачу декомпозиции монолитного интерфейса на независимые, автономно разрабатываемые и развертываемые подсистемы. Исторические попытки изоляции имели непреодолимые дефекты:
Технология Module Federation (Федерация модулей) обеспечивает бесшовную интеграцию удаленных модулей на уровне рантайма без пересборки контейнера.
Спецификация Module Federation v2 (реализованная в экосистеме Rspack / Rsbuild через @module-federation/enhanced) выводит архитектуру на новый уровень:
@module-federation/runtime) для перехвата загрузки чанков, инъекции заголовков авторизации и Service Discovery.В архитектуре Module Federation приложения делятся на две базовые роли:
Приложение может одновременно выступать и как Host, и как Remote (двунаправленная федерация).
┌──────────────────────────────────────────────────────────────────────────────────┐
│ HOST APPLICATION (Shell) │
│ http://app.enterprise.com │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ Global Context Providers: AuthContext, QueryClientProvider, ThemeProvider │ │
│ └──────────────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────▼─────────────────────────────────────┐ │
│ │ Module Federation v2 Runtime │ │
│ │ (Singleton Registry: [email protected], [email protected]) │ │
│ └──────────────────┬───────────────────────────────────┬─────────────────────┘ │
│ │ │ │
└─────────────────────┼───────────────────────────────────┼────────────────────────┘
│ (fetch remoteEntry.js) │ (fetch remoteEntry.js)
▼ ▼
┌───────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ REMOTE 1: Analytics Dashboard │ │ REMOTE 2: Billing & Checkout │
│ http://analytics.cdn.internal │ │ http://billing.cdn.internal │
│ Exposes: './DashboardWidget' │ │ Exposes: './CheckoutFlow' │
│ Shared: react, react-dom │ │ Shared: react, react-dom │
└───────────────────────────────────────┘ └──────────────────────────────────────┘Главным вызовом распределенного рантайма является предотвращение дублирования разделяемых библиотек (Shared Scope). Если Host и Remote загрузят каждый свою копию react или react-dom, React 19 выбросит фатальную ошибку нарушения контекста хуков (Invalid hook call или Multiple React instances detected).
Создание Remote-сервиса с помощью плагинов @rsbuild/plugin-react и @module-federation/rsbuild-plugin.
Определение точек входа компонентов, назначение react и react-dom строгими синглтонами.
Сборка продакшен-бандла, генерирующего метаданные версий и чанков для рантайма.
Загрузка артефактов Remote в изолированный бакет хранилища с настройкой CORS заголовков.
Подключение удаленного модуля через динамический lazy-импорт с изолированной обработкой ошибок.
Rsbuild — это высокопроизводительный инструмент сборки на базе Rust-движка Rspack, полностью совместимый со спецификацией Module Federation v2:
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'analytics_remote',
filename: 'remoteEntry.js',
// Экспортируемые компоненты для хост-приложений
exposes: {
'./DashboardWidget': './src/widgets/DashboardWidget.tsx',
'./MetricsSummary': './src/widgets/MetricsSummary.tsx',
},
// Управление разделяемыми синглтонами
shared: {
react: {
singleton: true,
requiredVersion: '^19.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
},
},
}),
],
server: {
port: 3001,
},
});import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'host_shell',
// Декларация удаленных микрофронтендов через manifest
remotes: {
analytics_remote: 'analytics_remote@http://localhost:3001/mf-manifest.json',
},
shared: {
react: {
singleton: true,
eager: true, // В хосте React должен быть доступен синхронно
requiredVersion: '^19.0.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^19.0.0',
},
},
}),
],
server: {
port: 3000,
},
});Для безопасного импорта экспортированных компонентов удаленного микрофронтенда в Host-приложении настраиваются декларации типов:
declare module 'analytics_remote/DashboardWidget' {
import type { FC } from 'react';
export interface DashboardWidgetProps {
tenantId: string;
refreshIntervalMs?: number;
onMetricSelect?: (metricId: string) => void;
}
const DashboardWidget: FC<DashboardWidgetProps>;
export default DashboardWidget;
}Сетевые сбои, тайм-ауты CDN или ошибки 404/500 при загрузке удаленного чанка remoteEntry.js не должны приводить к падению всего Shell-приложения. Каждое микроприложение обязано изолироваться через специализированный Error Boundary:
Пользователь переходит на URL /analytics. React Router монтирует Lazy-компонент.
Рантайм запрашивает mf-manifest.json с CDN удаленного сервиса и проверяет хэши зависимостей.
Удаленный модуль связывается с общим синглтоном React 19 хоста без повторной загрузки ядра.
В случае сетевой аварии или сбоя скрипта хост отображает локальный fallback-виджет.
Какую ключевую роль выполняет файл mf-manifest.json в Module Federation v2 по сравнению с прямым подключением remoteEntry.js в v1?
Ниже представлен production-компонент загрузки удаленного модуля, объединяющий React 19 Suspense, кастомный ErrorBoundary, механизм автоматических повторных попыток загрузки (Exponential Backoff) и типизацию:
import React, { Component, Suspense, lazy, type ReactNode } from 'react';
interface ErrorBoundaryProps {
fallback: (error: Error, reset: () => void) => ReactNode;
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class MicrofrontendErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public override state: ErrorBoundaryState = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
public override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.error('[Microfrontend Crash Alert]:', error, errorInfo);
}
private handleReset = (): void => {
this.setState({ hasError: false, error: null });
};
public override render(): ReactNode {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error, this.handleReset);
}
return this.props.children;
}
}
// Утилита динамического импорта с автоматическим ретраем при сбоях сети CDN
export function retryDynamicImport<T>(
importFn: () => Promise<T>,
retriesLeft = 3,
intervalMs = 1000
): Promise<T> {
return new Promise((resolve, reject) => {
importFn()
.then(resolve)
.catch((error: unknown) => {
if (retriesLeft <= 1) {
reject(error instanceof Error ? error : new Error(String(error)));
return;
}
setTimeout(() => {
retryDynamicImport(importFn, retriesLeft - 1, intervalMs * 1.5)
.then(resolve)
.catch(reject);
}, intervalMs);
});
});
}
interface FederatedLoaderProps<TProps extends Record<string, unknown>> {
load: () => Promise<{ default: React.ComponentType<TProps> }>;
props: TProps;
moduleName: string;
fallbackUi?: ReactNode;
}
export function FederatedComponentLoader<TProps extends Record<string, unknown>>({
load,
props,
moduleName,
fallbackUi,
}: FederatedLoaderProps<TProps>): React.JSX.Element {
const LazyComponent = React.useMemo(() => {
return lazy(() => retryDynamicImport(load));
}, [load]);
return (
<MicrofrontendErrorBoundary
fallback={(error, reset) => (
<div className="p-6 rounded-xl border border-danger/30 bg-danger/5 text-text-primary flex flex-col gap-3">
<div className="flex items-center gap-2 text-danger font-bold">
<span>⚠️ Ошибка загрузки микрофронтенда: {moduleName}</span>
</div>
<p className="text-sm text-text-muted">{error.message}</p>
<button
type="button"
onClick={reset}
className="self-start px-4 py-2 rounded-lg bg-bg-elevated border border-border hover:border-accent-cyan text-xs font-semibold text-accent-cyan transition-colors"
>
Повторить попытку подключения
</button>
</div>
)}
>
<Suspense
fallback={
fallbackUi || (
<div className="p-8 rounded-xl border border-border bg-bg-surface flex items-center justify-center">
<div className="flex items-center gap-3 text-accent-cyan text-sm font-mono animate-pulse">
<span className="w-2 h-2 rounded-full bg-accent-cyan" />
Инициализация удаленного сервиса {moduleName}...
</div>
</div>
)
}
>
<LazyComponent {...props} />
</Suspense>
</MicrofrontendErrorBoundary>
);
}Если в конфигурациях Host и Remote не указан флаг singleton: true для библиотек react и react-dom, каждый Remote загрузит свой экземпляр React. Это приводит к разрыву контекста React (Context API вернет значение по умолчанию) и ошибкам несовместимости хуков.
// ❌ Антипаттерн: отсутствие singleton создает дубликаты React в памяти
shared: {
react: { requiredVersion: '^19.0.0' },
'react-dom': { requiredVersion: '^19.0.0' },
}
// ✅ Правильное решение: строгий синглтон с проверкой версии
shared: {
react: {
singleton: true,
requiredVersion: '^19.0.0',
strictVersion: true,
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
strictVersion: true,
},
}Если Host и Remote используют классический CSS или разные конфигурации Tailwind CSS без изоляции пространств имен (Prefixing), стили удаленного модуля переопределят оформление главного приложения.
/* ❌ Антипаттерн: глобальный селектор кнопки внутри Remote */
button {
background-color: #ef4444; /* Сломает все кнопки на страницах хоста! */
}
/* ✅ Правильное решение: Использование Tailwind v4 prefix или CSS Modules / Scoped CSS */
@theme prefix(mf-analytics) {
/* Все утилиты изолированы префиксом */
}Если Host-приложение запускается синхронно, а React объявлен как асинхронный shared-модуль без eager: true, приложение упадет с ошибкой Shared module is not available for eager consumption.
Решение: использовать паттерн Bootstrap (import('./bootstrap')) в точке входа приложения, либо объявлять React в Host с параметром eager: true.