В ентерпрайз-екосистемах із десятками розподілених продуктових команд монолітні 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.