Розробка архітектури мікрофронтенд веб-додатків
Мікрофронтенди — це організаційний патерн, а не технологія. Суть: велика frontend-програма розбивається на незалежні частини, якими керують окремі команди. Кожна частина розробляється, тестується та розгортається самостійно.
Це виправдано, коли над одною програмою працюють 4+ команди, коли монолітна система стала вузьким місцем розгортання, або коли різні частини програми розвиваються у принципово різних темпах.
Що входить до роботи
Аналіз предметної області та кордонів між командами, вибір підходу інтеграції, проектування архітектури shell, налаштування спільних залежностей, дизайн-система, комунікація між мікрофронтендами, CI/CD-стратегія, документація.
Підходи інтеграції — порівняння
| Підхід | Build-time | Run-time | Ізоляція | Складність |
|---|---|---|---|---|
| NPM packages | Так | Ні | Ні | Низька |
| Module Federation | Ні | Так | Часткова | Середня |
| iframes | Ні | Так | Повна | Низька |
| Web Components | Ні | Так | CSS | Середня |
| Single-SPA | Ні | Так | Часткова | Висока |
Для більшості проектів оптимальні Module Federation (якщо все на React/Vue) або Single-SPA (якщо команди використовують різні фреймворки).
Крок 1 — Аналіз кордонів домену
Кордон мікрофронтенду — це кордон 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 — тонка оболонка без бізнес-логіки. Відповідає лише за:
- маршрутизацію верхнього рівня
- завантаження мікрофронтендів
- спільні сервіси (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 — Спільні контракти
Мікрофронтенди спілкуються через контракти. Контракт потрібно версіонувати та підтримувати зворотну сумісність:
// 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 — Спільні залежності та дизайн-система
Дизайн-система — окремий пакет, який все мікрофронтенди отримують як npm-залежність:
packages/
ui/ — компоненти, токени, іконки
src/
components/
tokens/
icons/
package.json
contracts/ — типи подій та інтерфейсів
shared-config/ — eslint, tsconfig, prettier базові конфіги
У Module Federation UI-пакет робимо singleton: true — все мікрофронтенди використовують одну версію:
// у кожному webpack.config.js / vite.config.ts
shared: {
'@company/ui': { singleton: true, requiredVersion: '^2.0.0' },
react: { singleton: true },
'react-dom': { singleton: true },
}
Крок 5 — Стратегія маршрутизації
Два підходи до маршрутизації:
Централізований (shell володіє маршрутами верхнього рівня):
shell: /products → завантажує CatalogApp
catalog: /products/:id, /products?search=
Делегований (кожен мікрофронтенд володіє своїми маршрутами):
shell: /* → передає в CatalogApp або CheckoutApp
catalog: обробляє /products/*, /categories/*
При використанні React Router рекомендую централізований: shell визначає верхній /products/*, всередині CatalogApp — свої вкладені Routes.
Крок 6 — Аутентифікація
Auth-логіка — в shell або в окремому auth-мікрофронтенді. Решта отримують тільки факт авторизації:
// 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>
)
}
// У мікрофронтенді — тільки 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 за кожним мікрофронтендом
import { onLCP, onFID, onCLS } from 'web-vitals'
onLCP((metric) => analytics.track('LCP', { value: metric.value, remote: 'catalog' }))
Типові помилки при переході
Занадто дрібне розділення — мікрофронтенд з 3 компонентів не виправданий операційним навантаженням.
Немає контрактів — команди починають залежати від внутрішньої реалізації один одного.
Несинхронізовані версії спільних залежностей — два React на сторінці → два VDOM, баги з hooks, розпухлий bundle.
Прямі імпорти між мікрофронтендами — порушує ізоляцію та робить незалежний розпуск неможливим.
Що ми робимо
Проводимо аналіз кордонів домену разом з product/engineering командами, вибираємо підхід інтеграції, проектуємо shell та пакет контрактів, налаштовуємо Module Federation або Single-SPA, організовуємо CI/CD з незалежним розгортанням, налаштовуємо моніторинг remote-завантаження.
Терміни: 10–20 днів — архітектура, реалізація shell, налаштування 2–3 мікрофронтендів, CI/CD. Подальший перенос логіки — в рамках окремих завдань.







