Разработка микрофронтендной архитектуры веб-приложения
Микрофронтенды — это организационный паттерн, а не технология. Суть: большое frontend-приложение разбивается на независимые части, которыми владеют отдельные команды. Каждая часть разрабатывается, тестируется и деплоится самостоятельно.
Это оправдано, когда над одним приложением работают 4+ команды, когда монолит стал узким местом для деплоя, или когда разные части приложения развиваются в принципиально разных темпах.
Что входит в работу
Анализ предметной области и границ между командами, выбор интеграционного подхода, проектирование shell-архитектуры, выстраивание shared-зависимостей, дизайн-системы, коммуникации между microfrontends, CI/CD-стратегия, документация.
Интеграционные подходы — сравнение
| Подход | Build-time | Run-time | Изоляция | Сложность |
|---|---|---|---|---|
| NPM packages | Да | Нет | Нет | Низкая |
| Module Federation | Нет | Да | Частичная | Средняя |
| iframes | Нет | Да | Полная | Низкая |
| Web Components | Нет | Да | CSS | Средняя |
| Single-SPA | Нет | Да | Частичная | Высокая |
Для большинства проектов оптимальны Module Federation (если всё на React/Vue) или Single-SPA (если команды используют разные фреймворки).
Шаг 1 — Анализ доменных границ
Граница microfrontend — это граница bounded context в бизнес-домене, а не техническая удобность:
E-commerce платформа:
├── Catalog Team → /products, /categories, /search
├── Cart Team → /cart, mini-cart widget
├── Checkout Team → /checkout, /payment
├── Account Team → /profile, /orders, /addresses
└── Platform Team → shell, auth, design system, analytics
Плохое разбиение: по техническому стеку (header/sidebar/content), по компонентам UI, по слоям (API/state/view).
Шаг 2 — Shell-приложение
Shell — тонкая оболочка без бизнес-логики. Отвечает только за:
- маршрутизацию верхнего уровня
- загрузку microfrontend-ов
- shared-сервисы (auth, analytics)
- навигацию и layout
// apps/shell/src/App.tsx
import React, { Suspense, lazy } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { Shell } from './components/Shell'
import { AuthGuard } from './guards/AuthGuard'
// lazy — загружаем только то, что нужно
const CatalogApp = lazy(() => import('catalog/App'))
const CheckoutApp = lazy(() => import('checkout/App'))
const AccountApp = lazy(() => import('account/App'))
export function App() {
return (
<Shell>
<Routes>
<Route path="/" element={<Navigate to="/products" replace />} />
<Route
path="/products/*"
element={
<Suspense fallback={<AppSkeleton name="Каталог" />}>
<CatalogApp />
</Suspense>
}
/>
<Route
path="/checkout/*"
element={
<AuthGuard>
<Suspense fallback={<AppSkeleton name="Оформление" />}>
<CheckoutApp />
</Suspense>
</AuthGuard>
}
/>
<Route
path="/account/*"
element={
<AuthGuard>
<Suspense fallback={<AppSkeleton name="Личный кабинет" />}>
<AccountApp />
</Suspense>
</AuthGuard>
}
/>
</Routes>
</Shell>
)
}
Шаг 3 — Shared-контракты
Microfrontends общаются через контракты. Контракт нужно версионировать и поддерживать обратную совместимость:
// packages/contracts/src/events.ts
// Типизированные события — опубликовываются как npm-пакет
export interface CartEvents {
'cart:item-added': { productId: string; quantity: number; price: number }
'cart:item-removed': { productId: string }
'cart:cleared': Record<string, never>
'cart:checkout-started': { cartTotal: number; itemCount: number }
}
export interface AuthEvents {
'auth:login': { userId: string; roles: string[] }
'auth:logout': Record<string, never>
'auth:token-refreshed': { expiresAt: number }
}
// Типизированный event bus
type AllEvents = CartEvents & AuthEvents
export class EventBus {
private emitter = new EventTarget()
emit<K extends keyof AllEvents>(event: K, detail: AllEvents[K]) {
this.emitter.dispatchEvent(new CustomEvent(event as string, { detail }))
}
on<K extends keyof AllEvents>(event: K, handler: (detail: AllEvents[K]) => void) {
const listener = (e: Event) => handler((e as CustomEvent).detail)
this.emitter.addEventListener(event as string, listener)
return () => this.emitter.removeEventListener(event as string, listener)
}
}
export const eventBus = new EventBus()
Шаг 4 — Shared-зависимости и дизайн-система
Дизайн-система — отдельный пакет, который все microfrontends получают как npm-зависимость:
packages/
ui/ — компоненты, токены, иконки
src/
components/
tokens/
icons/
package.json
contracts/ — типы событий и интерфейсов
shared-config/ — eslint, tsconfig, prettier базовые конфиги
В Module Federation UI-пакет делаем singleton: true — все microfrontends используют одну версию:
// в каждом webpack.config.js / vite.config.ts
shared: {
'@company/ui': { singleton: true, requiredVersion: '^2.0.0' },
react: { singleton: true },
'react-dom': { singleton: true },
}
Шаг 5 — Routing стратегия
Два подхода к роутингу:
Централизованный (shell владеет роутами верхнего уровня):
shell: /products → загружает CatalogApp
catalog: /products/:id, /products?search=
Делегированный (каждый microfrontend владеет своими роутами):
shell: /* → передаёт в CatalogApp или CheckoutApp
catalog: обрабатывает /products/*, /categories/*
При использовании React Router рекомендую централизованный: shell определяет верхний /products/*, внутри CatalogApp — свои вложенные Routes.
Шаг 6 — Аутентификация
Auth-логика — в shell или в отдельном auth-microfrontend. Остальные получают только факт авторизации:
// packages/auth-client/src/index.ts
export interface AuthContext {
user: User | null
token: string | null
isAuthenticated: boolean
login: (credentials: Credentials) => Promise<void>
logout: () => void
hasPermission: (permission: string) => boolean
}
// AuthProvider в shell
export function AuthProvider({ children }: { children: React.ReactNode }) {
// логика хранения токена, refresh, logout при 401
const auth = useAuthState()
return (
<AuthContext.Provider value={auth}>
{children}
</AuthContext.Provider>
)
}
// В microfrontend — только useAuth()
// import { useAuth } from '@company/auth-client'
// const { user, hasPermission } = useAuth()
Шаг 7 — Обработка деградации
Remote может быть недоступен. Shell должен деградировать gracefully:
function withRemoteFallback<P extends object>(
remoteLoader: () => Promise<{ default: React.ComponentType<P> }>,
FallbackComponent: React.ComponentType
) {
const Remote = lazy(remoteLoader)
return function RemoteWithFallback(props: P) {
return (
<ErrorBoundary
onError={(error) => {
monitoring.captureException(error, { tags: { type: 'remote_load_failure' } })
}}
fallback={<FallbackComponent />}
>
<Suspense fallback={<Spinner />}>
<Remote {...props} />
</Suspense>
</ErrorBoundary>
)
}
}
const CatalogApp = withRemoteFallback(
() => import('catalog/App'),
() => <ServiceUnavailable name="Каталог" />
)
Шаг 8 — CI/CD стратегия
monorepo (или polyrepo):
apps/catalog/ → pipeline → CDN catalog.example.com
apps/checkout/ → pipeline → CDN checkout.example.com
apps/shell/ → pipeline → CDN example.com
shell хранит remoteEntry URL в конфиге, полученном с сервера:
/api/mf-config → { catalog: "https://catalog.example.com/...", ... }
# .github/workflows/catalog-deploy.yml
name: Catalog Deploy
on:
push:
branches: [main]
paths: ['apps/catalog/**', 'packages/ui/**']
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd apps/catalog && npm ci && npm test && npm run build
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to S3
run: aws s3 sync apps/catalog/dist s3://$CATALOG_BUCKET --delete
- name: Notify shell of new version
run: |
curl -X POST $SHELL_API/mf-config \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-d '{"catalog": "https://catalog.example.com/assets/remoteEntry.js"}'
Мониторинг микрофронтендов
// Отслеживаем загрузку каждого remote
window.addEventListener('unhandledrejection', (e) => {
if (e.reason?.message?.includes('Loading chunk')) {
monitoring.track('remote_load_failure', {
error: e.reason.message,
remote: detectRemoteFromStack(e.reason.stack),
})
}
})
// Web Vitals по каждому microfrontend
import { onLCP, onFID, onCLS } from 'web-vitals'
onLCP((metric) => analytics.track('LCP', { value: metric.value, remote: 'catalog' }))
Типичные ошибки при переходе
Слишком мелкое разбиение — microfrontend из 3 компонентов не оправдан операционной нагрузкой.
Нет контрактов — команды начинают зависеть от внутренней реализации друг друга.
Несинхронизированные версии shared-зависимостей — два React на странице → два VDOM, баги с hooks, раздутый bundle.
Прямые импорты между microfrontends — нарушает изоляцию и делает независимый деплой невозможным.
Что делаем
Проводим анализ доменных границ вместе с product/engineering командами, выбираем интеграционный подход, проектируем shell и пакет контрактов, настраиваем Module Federation или Single-SPA, организуем CI/CD с независимым деплоем, настраиваем мониторинг remote-загрузок.
Срок: 10–20 дней — архитектура, реализация shell, настройка 2–3 microfrontends, CI/CD. Дальнейший перенос логики — в рамках отдельных задач.







