Микрофронтенды с React (micro-frontends)

16 июня 2026
Автор

Олег Марков

Введение

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

В этой статье вы разберёте, что такое микрофронтенды и когда их стоит применять, изучите все основные подходы к реализации, получите детальный пример с Webpack Module Federation и React, узнаете о коммуникации между приложениями, роутинге и шаринге зависимостей, а также познакомитесь с практическими советами и антипаттернами.

Что такое микрофронтенды и зачем они нужны

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

Ключевые принципы

  • Технологическая независимость. Каждая команда может выбирать свой стек (React, Vue, Angular) внутри своего микрофронтенда, хотя на практике лучше придерживаться единого фреймворка.
  • Изолированные кодовые базы. Нет общих runtime-зависимостей, каждое приложение компилируется отдельно.
  • Независимый деплой. Изменение одного микрофронтенда не требует пересборки других.
  • Отдельные команды. Каждая команда владеет своим продуктовым доменом — от базы данных до UI.

Когда микрофронтенды оправданы

Микрофронтенды — не серебряная пуля. Они оправданы когда:

  • Над проектом работают несколько независимых команд (5+ человек во фронтенде).
  • Части приложения развиваются с разными скоростями и имеют разные релизные циклы.
  • Необходимо постепенно мигрировать устаревший монолит на новый стек.
  • Части приложения переиспользуются в нескольких продуктах.

Для небольших команд (2-4 разработчика) хорошо структурированный монолит проще в поддержке и эффективнее.

Основные подходы к реализации

1. Webpack Module Federation

Наиболее зрелый и мощный подход для React-экосистемы. Позволяет в runtime загружать код из отдельно задеплоенных приложений и шарить зависимости (например, единственный экземпляр React для всех микрофронтендов).

Плюсы: Нативная интеграция с экосистемой Webpack, шаринг зависимостей, горячая замена модулей. Минусы: Привязка к Webpack (хотя есть порты для Vite), сложная отладка, требует продуманной версионности.

2. iframes

Самый простой способ изоляции — каждый микрофронтенд живёт в отдельном iframe.

<!-- Shell App -->
<iframe src="https://catalog.example.com" id="catalog-frame"></iframe>
<iframe src="https://checkout.example.com" id="checkout-frame"></iframe>

Плюсы: Полная изоляция CSS и JavaScript, независимость стека. Минусы: Плохой UX (независимая прокрутка, URL не синхронизирован), сложная коммуникация через postMessage, проблемы с SEO, производительность.

3. Web Components

Каждый микрофронтенд оборачивается в кастомный HTML-элемент.

// React-приложение как Web Component
class CatalogApp extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('div');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
    ReactDOM.render(<CatalogRoot />, mountPoint);
  }

  disconnectedCallback() {
    ReactDOM.unmountComponentAtNode(this.shadowRoot.firstChild);
  }
}

customElements.define('catalog-app', CatalogApp);
<!-- Shell App -->
<catalog-app data-user-id="42"></catalog-app>

Плюсы: Стандартный браузерный API, изоляция через Shadow DOM, технологически нейтральный контракт. Минусы: Shadow DOM усложняет глобальные стили, React плохо работает с Shadow DOM без workarounds, server-side rendering затруднён.

4. NPM-пакеты (Build-time integration)

Каждый микрофронтенд публикуется как npm-пакет и импортируется в shell во время сборки.

{
  "dependencies": {
    "@company/catalog-app": "^2.3.0",
    "@company/checkout-app": "^1.8.0"
  }
}

Плюсы: Простота, типизация, понятный процесс. Минусы: Нет независимого деплоя — изменение любого пакета требует пересборки shell. Фактически это монорепо, не настоящие микрофронтенды.

5. Server-Side Composition (Edge-side Includes)

Сервер или CDN-edge собирает HTML из нескольких источников перед отдачей клиенту.

# Nginx SSI
location /page {
  ssi on;
  root /var/www;
}
<!-- page.html -->
<div id="header">
  <!--#include virtual="https://header.example.com/fragment" -->
</div>
<div id="catalog">
  <!--#include virtual="https://catalog.example.com/fragment" -->
</div>

Плюсы: Отличный First Contentful Paint, хороший SEO, нет JavaScript-зависимостей. Минусы: Сложность инфраструктуры, ограниченная интерактивность, зависимость от uptime всех сервисов.

Webpack Module Federation с React — детальный пример

Module Federation появился в Webpack 5 и де-факто стал стандартом для микрофронтендов в экосистеме React. Рассмотрим архитектуру из трёх приложений:

  • shell (host) — контейнер, который загружает остальные приложения
  • catalog (remote) — каталог товаров
  • checkout (remote) — оформление заказа

Структура проекта

microfrontend-demo/
├── shell/
│   ├── src/
│   │   ├── App.tsx
│   │   ├── bootstrap.tsx
│   │   └── index.ts
│   ├── webpack.config.js
│   └── package.json
├── catalog/
│   ├── src/
│   │   ├── CatalogApp.tsx
│   │   ├── bootstrap.tsx
│   │   └── index.ts
│   ├── webpack.config.js
│   └── package.json
└── checkout/
    ├── src/
    │   ├── CheckoutApp.tsx
    │   ├── bootstrap.tsx
    │   └── index.ts
    ├── webpack.config.js
    └── package.json

Настройка Remote (catalog)

Установите необходимые зависимости:

npm install react react-dom
npm install --save-dev webpack webpack-cli webpack-dev-server \
  babel-loader @babel/core @babel/preset-react @babel/preset-typescript \
  html-webpack-plugin ts-loader typescript

Настройте webpack.config.js для микрофронтенда catalog:

// catalog/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  output: {
    // publicPath важен: браузер использует его для загрузки чанков
    publicPath: 'http://localhost:3001/',
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    // Для production используйте реальный URL CDN/сервера
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      // Уникальное имя этого микрофронтенда
      name: 'catalog',
      // Имя файла-манифеста, который shell будет загружать
      filename: 'remoteEntry.js',
      // Экспортируемые модули
      exposes: {
        // './CatalogApp' — имя, под которым shell импортирует компонент
        // './src/CatalogApp' — реальный путь к файлу
        './CatalogApp': './src/CatalogApp',
      },
      // Шаринг зависимостей с другими микрофронтендами
      shared: {
        react: {
          // singleton: true гарантирует единственный экземпляр React
          singleton: true,
          // requiredVersion берём из package.json
          requiredVersion: require('./package.json').dependencies.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: require('./package.json').dependencies['react-dom'],
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  devServer: {
    port: 3001,
    // Важно: разрешить CORS для shell
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
};

Создайте компонент для экспорта:

// catalog/src/CatalogApp.tsx
import React, { useState } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
}

const PRODUCTS: Product[] = [
  { id: 1, name: 'Ноутбук Pro', price: 89990 },
  { id: 2, name: 'Механическая клавиатура', price: 12990 },
  { id: 3, name: 'Монитор 4K', price: 54990 },
];

interface CatalogAppProps {
  // Пропсы для коммуникации с shell
  onAddToCart?: (product: Product) => void;
  userId?: string;
}

const CatalogApp: React.FC<CatalogAppProps> = ({ onAddToCart, userId }) => {
  const [selectedId, setSelectedId] = useState<number | null>(null);

  const handleAddToCart = (product: Product) => {
    setSelectedId(product.id);
    // Передаём событие в shell через пропс
    onAddToCart?.(product);
  };

  return (
    <div style={{ padding: '20px', border: '2px solid #007bff', borderRadius: '8px' }}>
      <h2>Каталог товаров {userId && `(пользователь: ${userId})`}</h2>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
        {PRODUCTS.map(product => (
          <div
            key={product.id}
            style={{
              padding: '16px',
              border: selectedId === product.id ? '2px solid green' : '1px solid #ddd',
              borderRadius: '4px',
            }}
          >
            <h3>{product.name}</h3>
            <p>{product.price.toLocaleString('ru-RU')} ₽</p>
            <button onClick={() => handleAddToCart(product)}>
              В корзину
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default CatalogApp;

Важный паттерн — отдельный bootstrap.tsx:

// catalog/src/bootstrap.tsx
// Этот файл используется только для standalone-запуска catalog
import React from 'react';
import ReactDOM from 'react-dom/client';
import CatalogApp from './CatalogApp';

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<CatalogApp />);
// catalog/src/index.ts
// Динамический импорт bootstrap обязателен для Module Federation!
// Без него возникнет Shared module is not available for eager consumption
import('./bootstrap');

Настройка Host (shell)

// shell/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  output: {
    publicPath: 'http://localhost:3000/',
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      // Shell ничего не экспортирует, только потребляет remotes
      remotes: {
        // 'catalog' — имя для import(), должно совпадать с name в catalog/webpack.config.js
        // 'catalog@...' — URL remoteEntry.js
        catalog: 'catalog@http://localhost:3001/remoteEntry.js',
        checkout: 'checkout@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: require('./package.json').dependencies.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: require('./package.json').dependencies['react-dom'],
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  devServer: {
    port: 3000,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
};

Главный компонент shell с ленивой загрузкой:

// shell/src/App.tsx
import React, { Suspense, lazy, useState } from 'react';

// Ленивая загрузка микрофронтендов — они будут скачаны только когда потребуются
const CatalogApp = lazy(() => import('catalog/CatalogApp'));
const CheckoutApp = lazy(() => import('checkout/CheckoutApp'));

// TypeScript не знает о remote-модулях — нужны декларации
// Создайте src/declarations.d.ts:
// declare module 'catalog/CatalogApp';
// declare module 'checkout/CheckoutApp';

interface CartItem {
  id: number;
  name: string;
  price: number;
}

const App: React.FC = () => {
  const [cart, setCart] = useState<CartItem[]>([]);
  const [currentPage, setCurrentPage] = useState<'catalog' | 'checkout'>('catalog');

  const handleAddToCart = (product: CartItem) => {
    setCart(prev => {
      // Проверяем, нет ли уже этого товара в корзине
      const exists = prev.find(item => item.id === product.id);
      if (exists) return prev;
      return [...prev, product];
    });
  };

  return (
    <div>
      <header style={{ padding: '16px', backgroundColor: '#343a40', color: 'white' }}>
        <h1>Мой Магазин (Shell)</h1>
        <nav>
          <button
            onClick={() => setCurrentPage('catalog')}
            style={{ marginRight: '8px', color: currentPage === 'catalog' ? 'yellow' : 'white' }}
          >
            Каталог
          </button>
          <button
            onClick={() => setCurrentPage('checkout')}
            style={{ color: currentPage === 'checkout' ? 'yellow' : 'white' }}
          >
            Корзина ({cart.length})
          </button>
        </nav>
      </header>

      <main style={{ padding: '20px' }}>
        {/* ErrorBoundary обязателен — если remote упал, остальное должно работать */}
        <Suspense fallback={<div>Загружаем каталог...</div>}>
          {currentPage === 'catalog' && (
            <CatalogApp
              onAddToCart={handleAddToCart}
              userId="user-123"
            />
          )}
        </Suspense>

        <Suspense fallback={<div>Загружаем оформление заказа...</div>}>
          {currentPage === 'checkout' && (
            <CheckoutApp
              cart={cart}
              onRemoveFromCart={(id) => setCart(prev => prev.filter(item => item.id !== id))}
            />
          )}
        </Suspense>
      </main>
    </div>
  );
};

export default App;
// shell/src/declarations.d.ts
// TypeScript декларации для remote-модулей
declare module 'catalog/CatalogApp' {
  import React from 'react';
  interface CatalogAppProps {
    onAddToCart?: (product: { id: number; name: string; price: number }) => void;
    userId?: string;
  }
  const CatalogApp: React.FC<CatalogAppProps>;
  export default CatalogApp;
}

declare module 'checkout/CheckoutApp' {
  import React from 'react';
  interface CheckoutAppProps {
    cart: Array<{ id: number; name: string; price: number }>;
    onRemoveFromCart?: (id: number) => void;
  }
  const CheckoutApp: React.FC<CheckoutAppProps>;
  export default CheckoutApp;
}

Error Boundary для микрофронтендов

Всегда оборачивайте remote-компоненты в Error Boundary — если один микрофронтенд упал, остальные должны продолжать работать:

// shell/src/MicroFrontendErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';

interface Props {
  name: string;
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class MicroFrontendErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error(`[MFE Error] ${this.props.name}:`, error, errorInfo);
    // Отправьте ошибку в систему мониторинга (Sentry, etc.)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div style={{ padding: '20px', border: '1px solid red', borderRadius: '4px' }}>
          <h3>Микрофронтенд "{this.props.name}" недоступен</h3>
          <p>Пожалуйста, обновите страницу или попробуйте позже.</p>
        </div>
      );
    }
    return this.props.children;
  }
}

export default MicroFrontendErrorBoundary;

Используйте его в App:

// В App.tsx
<MicroFrontendErrorBoundary name="Каталог">
  <Suspense fallback={<div>Загружаем...</div>}>
    <CatalogApp onAddToCart={handleAddToCart} userId="user-123" />
  </Suspense>
</MicroFrontendErrorBoundary>

Коммуникация между микрофронтендами

Один из самых сложных аспектов микрофронтендов — это передача данных между изолированными приложениями. Существует несколько подходов.

1. Props и callbacks (рекомендуется)

Самый простой и типобезопасный способ — shell передаёт данные через пропсы, а микрофронтенды сообщают о событиях через колбэки. Этот подход уже продемонстрирован в примере выше.

// Shell передаёт данные и обработчики событий
<CatalogApp
  userId={currentUser.id}
  onAddToCart={handleAddToCart}
  theme={appTheme}
/>

Плюсы: Типобезопасность, предсказуемость, простота. Минусы: Все данные проходят через shell, иерархия жёсткая.

2. Custom Events (Browser Events API)

Микрофронтенды могут общаться через браузерные события, минуя shell:

// catalog/src/CatalogApp.tsx — отправка события
const handleAddToCart = (product: Product) => {
  // Отправляем кастомное событие на уровень document
  const event = new CustomEvent('catalog:product-added', {
    detail: { product },
    bubbles: true,
  });
  document.dispatchEvent(event);
};
// checkout/src/CheckoutApp.tsx — получение события
import { useEffect, useState } from 'react';

const CheckoutApp = () => {
  const [cart, setCart] = useState<Product[]>([]);

  useEffect(() => {
    const handleProductAdded = (event: CustomEvent) => {
      const { product } = event.detail;
      setCart(prev => [...prev, product]);
    };

    document.addEventListener('catalog:product-added', handleProductAdded as EventListener);

    return () => {
      document.removeEventListener('catalog:product-added', handleProductAdded as EventListener);
    };
  }, []);

  return <div>Корзина: {cart.length} товар(ов)</div>;
};

Плюсы: Микрофронтенды не зависят друг от друга напрямую, слабое связывание. Минусы: Нет типизации из коробки, сложно отлаживать, риск коллизий имён событий.

3. Shared State через SharedWorker или localStorage

Для более сложных сценариев можно использовать SharedWorker:

// shared/CartWorker.ts (компилируется отдельно)
let cart: any[] = [];

self.onmessage = (event) => {
  const { type, payload } = event.data;

  switch (type) {
    case 'ADD_TO_CART':
      cart = [...cart, payload.product];
      // Уведомляем всех подключённых клиентов
      (self as SharedWorkerGlobalScope).ports.forEach(port => {
        port.postMessage({ type: 'CART_UPDATED', cart });
      });
      break;
    case 'GET_CART':
      event.ports[0]?.postMessage({ type: 'CART_UPDATED', cart });
      break;
  }
};
// В любом микрофронтенде
const worker = new SharedWorker('/cart-worker.js');
worker.port.start();

// Получение обновлений
worker.port.onmessage = (event) => {
  if (event.data.type === 'CART_UPDATED') {
    setCart(event.data.cart);
  }
};

// Отправка действия
const addToCart = (product: Product) => {
  worker.port.postMessage({ type: 'ADD_TO_CART', payload: { product } });
};

4. URL как источник правды

Используйте URL как единственный источник состояния для навигационных данных:

// Текущий маршрут, фильтры, поиск — в URL
// catalog?category=laptops&brand=apple&page=2
const CatalogApp = () => {
  // Читаем параметры из URL
  const searchParams = new URLSearchParams(window.location.search);
  const category = searchParams.get('category');

  const handleCategoryChange = (newCategory: string) => {
    // Обновляем URL — все заинтересованные могут его читать
    const url = new URL(window.location.href);
    url.searchParams.set('category', newCategory);
    window.history.pushState({}, '', url.toString());
  };
};

Шаринг зависимостей

Без правильного шаринга каждый микрофронтенд включал бы свою копию React (~130KB). Module Federation решает эту проблему через секцию shared.

Стратегии шаринга

// webpack.config.js — расширенные настройки shared
new ModuleFederationPlugin({
  shared: {
    react: {
      // Один экземпляр React на всю страницу — обязательно!
      singleton: true,
      // Предупреждение вместо ошибки при несовместимых версиях
      strictVersion: false,
      requiredVersion: '^18.2.0',
      // eager: true — включить в основной чанк, не загружать асинхронно
      // Нужно только для bootstrap — для remotes оставьте false
      eager: false,
    },
    'react-dom': {
      singleton: true,
      strictVersion: false,
      requiredVersion: '^18.2.0',
    },
    // Роутер тоже должен быть singleton
    'react-router-dom': {
      singleton: true,
      requiredVersion: '^6.0.0',
    },
    // Большие библиотеки, которые используются везде
    'date-fns': {
      singleton: false, // Не singleton — разные версии совместимы
      requiredVersion: '^3.0.0',
    },
  },
})

Автоматическое определение shared-зависимостей

Вместо ручного перечисления можно использовать пакет @module-federation/utilities:

// webpack.config.js
const { shareAll } = require('@module-federation/utilities');

new ModuleFederationPlugin({
  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: false,
      requiredVersion: 'auto',
    }),
  },
})

Это удобно для больших проектов, но может привести к неожиданным singleton-конфликтам — используйте осторожно.

Роутинг в micro-frontend архитектуре

Роутинг в микрофронтендах требует чёткого разграничения ответственности.

Паттерн: Shell управляет верхнеуровневым роутингом

Shell отвечает за маршруты верхнего уровня и загружает соответствующий микрофронтенд. Внутренний роутинг — ответственность микрофронтенда.

// shell/src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const CatalogApp = lazy(() => import('catalog/CatalogApp'));
const CheckoutApp = lazy(() => import('checkout/CheckoutApp'));
const ProfileApp = lazy(() => import('profile/ProfileApp'));

const App = () => {
  return (
    <BrowserRouter>
      <Header />
      <Routes>
        {/* Shell маршрутизирует по первому сегменту пути */}
        <Route
          path="/catalog/*"
          element={
            <Suspense fallback={<Loader />}>
              <CatalogApp />
            </Suspense>
          }
        />
        <Route
          path="/checkout/*"
          element={
            <Suspense fallback={<Loader />}>
              <CheckoutApp />
            </Suspense>
          }
        />
        <Route
          path="/profile/*"
          element={
            <Suspense fallback={<Loader />}>
              <ProfileApp />
            </Suspense>
          }
        />
        <Route path="/" element={<HomePage />} />
      </Routes>
    </BrowserRouter>
  );
};
// catalog/src/CatalogApp.tsx — внутренний роутинг
import { Routes, Route, useNavigate } from 'react-router-dom';

const CatalogApp = () => {
  const navigate = useNavigate();

  return (
    <Routes>
      {/* Маршруты внутри /catalog/* */}
      <Route index element={<ProductList />} />
      <Route path="product/:id" element={<ProductDetail />} />
      <Route path="search" element={<SearchResults />} />
    </Routes>
  );
};

MemoryRouter для изолированных микрофронтендов

Когда микрофронтенд должен работать как standalone (для разработки и тестирования), используйте условный роутер:

// catalog/src/CatalogApp.tsx
import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom';

interface CatalogAppProps {
  // Флаг для standalone-режима (разработка/тестирование)
  standalone?: boolean;
  basePath?: string;
}

const CatalogApp: React.FC<CatalogAppProps> = ({ standalone = false, basePath = '/catalog' }) => {
  const Router = standalone ? MemoryRouter : React.Fragment;
  const routerProps = standalone ? { initialEntries: ['/'] } : {};

  // Когда встроен в shell — используем его BrowserRouter
  // Когда standalone — используем собственный MemoryRouter
  if (standalone) {
    return (
      <MemoryRouter>
        <CatalogRoutes />
      </MemoryRouter>
    );
  }

  // В shell роутинг уже предоставлен
  return <CatalogRoutes />;
};

const CatalogRoutes = () => (
  <Routes>
    <Route index element={<ProductList />} />
    <Route path="product/:id" element={<ProductDetail />} />
  </Routes>
);

Синхронизация URL между микрофронтендами

Проблема возникает, когда микрофронтенд меняет URL, а другие микрофронтенды должны об этом знать:

// Слушатель изменений URL — шаринг через кастомное событие
const useSyncedNavigation = (callback: (path: string) => void) => {
  useEffect(() => {
    const handlePopState = () => callback(window.location.pathname);

    window.addEventListener('popstate', handlePopState);

    // Патчим history.pushState для перехвата программной навигации
    const originalPushState = window.history.pushState.bind(window.history);
    window.history.pushState = (...args) => {
      originalPushState(...args);
      callback(window.location.pathname);
    };

    return () => {
      window.removeEventListener('popstate', handlePopState);
      window.history.pushState = originalPushState;
    };
  }, [callback]);
};

Module Federation с Vite

Для проектов на Vite используйте плагин @originjs/vite-plugin-federation:

npm install --save-dev @originjs/vite-plugin-federation
// catalog/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'catalog',
      filename: 'remoteEntry.js',
      exposes: {
        './CatalogApp': './src/CatalogApp',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    target: 'esnext', // Обязательно для Module Federation в Vite
    minify: false,
  },
});
// shell/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'shell',
      remotes: {
        catalog: 'http://localhost:5001/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    target: 'esnext',
  },
});

Плюсы и минусы микрофронтендов

Плюсы

Аспект Преимущество
Независимый деплой Каждая команда деплоит свой микрофронтенд независимо
Масштабирование команд Разные команды работают без конфликтов в коде
Технологическая свобода Каждый микрофронтенд может использовать свой стек
Изоляция сбоев Ошибка в одном MFE не роняет всё приложение
Инкрементальная миграция Монолит можно мигрировать по частям
Фокус команды Команда отвечает за конкретный бизнес-домен

Минусы

Аспект Проблема
Сложность инфраструктуры Нужен CI/CD для каждого MFE, service discovery, мониторинг
Дублирование кода Общий код сложно шарить без монорепо
Производительность Дополнительные HTTP-запросы за remoteEntry.js, риск дублирования бандлов
Консистентность UI Сложнее поддерживать единый дизайн между командами
Отладка Ошибки в runtime сложнее диагностировать
Overhead для малых команд Операционная сложность не окупается для команд до 5 человек

Best Practices

1. Версионируйте контракты

Определите явные интерфейсы (контракты) для взаимодействия между MFE. Публикуйте их как TypeScript-пакеты:

// @company/mfe-contracts (отдельный npm-пакет)
export interface CatalogAppProps {
  userId: string;
  onAddToCart: (product: CartProduct) => void;
  theme?: 'light' | 'dark';
}

export interface CartProduct {
  id: number;
  name: string;
  price: number;
  imageUrl?: string;
}

2. Дизайн-система как foundation

Выделите общие UI-компоненты (кнопки, формы, таблицы) в отдельную шаренную библиотеку. Это обеспечит консистентность UX без жёсткой связанности:

{
  "dependencies": {
    "@company/ui-kit": "^3.0.0"
  }
}

3. Независимая разработка и тестирование

Каждый MFE должен запускаться и тестироваться автономно. Для этого создайте standalone режим:

// catalog/src/standalone.tsx — только для разработки
import React from 'react';
import ReactDOM from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import CatalogApp from './CatalogApp';

// Мок данных для standalone-разработки
const mockCallbacks = {
  onAddToCart: (product: any) => console.log('Add to cart:', product),
};

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
  <MemoryRouter>
    <CatalogApp {...mockCallbacks} userId="dev-user" />
  </MemoryRouter>
);

4. Мониторинг и observability

Каждый MFE должен иметь собственный мониторинг с правильным контекстом:

// Добавьте MFE-контекст в логи ошибок
const initMonitoring = (mfeName: string) => {
  // Sentry, Datadog, etc.
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    beforeSend(event) {
      event.tags = { ...event.tags, microFrontend: mfeName };
      return event;
    },
  });
};

5. Graceful degradation

Всегда обрабатывайте недоступность remote:

// Используйте заглушки если remote недоступен
const loadRemoteComponent = async (componentPath: string) => {
  try {
    return await import(/* webpackChunkName: "remote" */ componentPath);
  } catch (error) {
    console.error(`Failed to load remote: ${componentPath}`, error);
    // Возвращаем заглушку или кешированную версию
    return { default: FallbackComponent };
  }
};

6. Единая точка конфигурации

Не хардкодьте URL remotes в webpack.config.js. Используйте переменные окружения и runtime-конфигурацию:

// webpack.config.js
new ModuleFederationPlugin({
  remotes: {
    catalog: `catalog@${process.env.CATALOG_URL || 'http://localhost:3001'}/remoteEntry.js`,
    checkout: `checkout@${process.env.CHECKOUT_URL || 'http://localhost:3002'}/remoteEntry.js`,
  },
})

Для более гибкого подхода используйте runtime configuration:

// shell/src/loadRemote.ts
interface RemoteConfig {
  [key: string]: string; // name -> URL
}

// Конфигурацию можно получить с API в runtime
const getRemoteConfig = async (): Promise<RemoteConfig> => {
  const response = await fetch('/api/mfe-config');
  return response.json();
};

// Динамическая загрузка remote
const loadRemote = async (remoteName: string, modulePath: string) => {
  const config = await getRemoteConfig();
  const remoteUrl = config[remoteName];

  // Загружаем remoteEntry.js
  await new Promise<void>((resolve, reject) => {
    const script = document.createElement('script');
    script.src = `${remoteUrl}/remoteEntry.js`;
    script.onload = () => resolve();
    script.onerror = reject;
    document.head.appendChild(script);
  });

  // Инициализируем контейнер
  const container = (window as any)[remoteName];
  await container.init(__webpack_share_scopes__.default);

  // Получаем нужный модуль
  const factory = await container.get(modulePath);
  return factory();
};

Реальные примеры кода

Загрузка пользовательских данных в shell и передача в MFE

// shell/src/hooks/useCurrentUser.ts
import { useState, useEffect } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

export const useCurrentUser = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/me')
      .then(res => res.json())
      .then(data => {
        setUser(data.user);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, []);

  return { user, loading };
};
// shell/src/App.tsx
const App = () => {
  const { user, loading } = useCurrentUser();

  if (loading) return <GlobalLoader />;

  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/catalog/*"
          element={
            <MicroFrontendErrorBoundary name="Каталог">
              <Suspense fallback={<SectionLoader />}>
                {/* Передаём пользовательский контекст в MFE */}
                <CatalogApp
                  userId={user?.id}
                  userRole={user?.role}
                  onAddToCart={handleAddToCart}
                />
              </Suspense>
            </MicroFrontendErrorBoundary>
          }
        />
      </Routes>
    </BrowserRouter>
  );
};

Тема и i18n через Context

Шарьте глобальные настройки через React Context, экспортированный из shell:

// shell/src/ThemeProvider.tsx — экспортируется как shared-модуль
import React, { createContext, useContext, useState, ReactNode } from 'react';

type Theme = 'light' | 'dark';
type Language = 'ru' | 'en';

interface AppContextValue {
  theme: Theme;
  language: Language;
  setTheme: (theme: Theme) => void;
  setLanguage: (lang: Language) => void;
}

const AppContext = createContext<AppContextValue | null>(null);

export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>('light');
  const [language, setLanguage] = useState<Language>('ru');

  return (
    <AppContext.Provider value={{ theme, language, setTheme, setLanguage }}>
      {children}
    </AppContext.Provider>
  );
};

export const useAppContext = () => {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useAppContext must be used within AppProvider');
  }
  return context;
};
// shell/webpack.config.js — экспортируем контекст как shared
new ModuleFederationPlugin({
  name: 'shell',
  exposes: {
    // MFE могут импортировать общий контекст из shell
    './AppContext': './src/ThemeProvider',
  },
  remotes: { /* ... */ },
  shared: { /* ... */ },
})
// catalog/src/CatalogApp.tsx — используем контекст из shell
// import { useAppContext } from 'shell/AppContext'; // Если шарим через MF
// Или используем пропсы — проще и надёжнее:

const CatalogApp: React.FC<CatalogAppProps & { theme?: 'light' | 'dark' }> = ({ theme = 'light' }) => {
  const styles = theme === 'dark'
    ? { background: '#1a1a1a', color: '#fff' }
    : { background: '#fff', color: '#000' };

  return (
    <div style={styles}>
      {/* ... */}
    </div>
  );
};

Пример CI/CD конфигурации (GitHub Actions)

# catalog/.github/workflows/deploy.yml
name: Deploy Catalog MFE

on:
  push:
    branches: [main]
    paths:
      - 'catalog/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: catalog/package-lock.json

      - name: Install dependencies
        working-directory: catalog
        run: npm ci

      - name: Build
        working-directory: catalog
        run: npm run build
        env:
          NODE_ENV: production

      - name: Deploy to CDN
        run: aws s3 sync catalog/dist/ s3://my-mfe-bucket/catalog/ --delete
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      # После деплоя catalog — shell обновляется автоматически
      # так как загружает remoteEntry.js в runtime

Инструменты экосистемы

Module Federation Manager

Для управления несколькими микрофронтендами используйте @module-federation/dashboard:

npm install @module-federation/dashboard-plugin

Nx для монорепо

Если все MFE в одном репозитории (монорепо), Nx предоставляет отличную интеграцию:

npx create-nx-workspace@latest my-mfe --preset=react
cd my-mfe
nx generate @nx/react:application catalog
nx generate @nx/react:application shell
// nx.json — настройка Module Federation
{
  "generators": {
    "@nx/react": {
      "application": {
        "bundler": "webpack"
      }
    }
  }
}

single-spa

Для оркестрации микрофронтендов от разных команд с разными фреймворками используйте single-spa:

// shell/src/index.js — single-spa root config
import { registerApplication, start } from 'single-spa';

registerApplication({
  name: '@company/catalog',
  app: () => System.import('@company/catalog'),
  activeWhen: ['/catalog'],
  customProps: { userId: getCurrentUserId() },
});

registerApplication({
  name: '@company/checkout',
  app: () => System.import('@company/checkout'),
  activeWhen: ['/checkout'],
});

start({ urlRerouteOnly: true });

Заключение

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

Ключевые выводы:

  • Начинайте с монолита. Разбивайте на микрофронтенды только когда команда и продукт переросли монолитный подход.
  • Module Federation — текущий стандарт для React-экосистемы, доступен и для Vite.
  • Паттерн bootstrap.tsx обязателен — без него возникают ошибки с eager shared modules.
  • Error Boundary + Suspense — обязательная обёртка для всех remote-компонентов.
  • Пропсы и callbacks — самый безопасный способ коммуникации, Custom Events — для слабосвязанного взаимодействия.
  • Синглтоны для React и роутера — без singleton: true вы получите несколько экземпляров и баги.
  • Дизайн-система как фундамент — без неё UI станет несогласованным.
  • CI/CD для каждого MFE независимо — иначе теряется главное преимущество.
Стрелочка влевоMobX — реактивное управление состоянием в ReactЗагрузка и индикаторыСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseInsertionEffect — внедрение стилей до мутаций DOMuseLayoutEffect в React — эффект до отрисовкиuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровОптимизация рендеринга в React: от теории к глубокой практикеuseActionState в React 19Как и зачем использовать React HooksЧто такое useRef и как его применять в ReactУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactFragment в React: группировка элементов без лишних узлов DOMПорталы в React: рендер компонентов вне иерархии DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий