Архітектура мікрофронтендів
Архітектура мікрофронтендів розбиває фронтенд-застосунок на кілька незалежних веб-застосунків, кожен з яких належить окремій команді і завантажується в єдиний UI під час виконання.
Теорія
TL;DR
- Аналогія: як конструктор Lego - кожна команда будує і випускає свій блок незалежно; shell-застосунок збирає їх разом без перебудови всього проекту
- Головна відмінність від моноліту: команди деплоять окремо, обирають власні фреймворки (React у команди A, Vue у команди B), і зламаний CSS однієї команди не руйнує інтерфейс іншої
- Module Federation (Webpack 5) - домінуючий підхід у продакшені; single-spa - інший популярний варіант
- Варто використовувати якщо 50+ розробників у кількох командах; для команди до 20 людей зайве
- Реальна ціна - інфраструктура: кожен MF потребує власного CI/CD, моніторингу та версіонування
Швидкий приклад
Shell-застосунок через single-spa завантажує два мікрофронтенди під час виконання:
<!-- index.html - shell завантажує MF1 (React) і MF2 (Vue) -->
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/single-spa@latest/lib/system/single-spa.min.js"></script>
</head>
<body>
<div id="navbar"></div> <!-- React MF монтується тут -->
<div id="dashboard"></div> <!-- Vue MF монтується тут -->
<script>
// Реєструємо navbar MF (React, завжди активний)
System.import('http://localhost:3001/navbar.js').then(navbar =>
singleSpa.registerApplication('navbar', navbar.default, () => true));
// Реєструємо dashboard MF (Vue, активний тільки на /dash)
System.import('http://localhost:3002/dashboard.js').then(dashboard =>
singleSpa.registerApplication('dashboard', dashboard.default,
() => window.location.pathname.startsWith('/dash')));
singleSpa.start();
</script>
</body>
</html>Shell завантажує бандл кожного MF з окремого сервера. Перейди на /dash - single-spa монтує Vue-застосунок у #dashboard. Перейди назад - він розмонтовується. React navbar при цьому залишається активним увесь час.
Ключова відмінність від моноліту
У моноліті всі команди спільно використовують один репозиторій, одну збірку і один деплой. Поганий CSS команди A ламає кнопки команди B. Апдейт залежності команди C руйнує тести команди D. У п'ятницю ніхто нічого не викочує. Мікрофронтенди розривають цей зв'язок на рівні інфраструктури: кожен застосунок має власний бандл, власну точку деплою та власний ритм релізів. Команда A переходить на React 18, не чекаючи поки команда B завершить міграцію з Angular 16.
Підходи до інтеграції
Є чотири основних підходи, кожен зі своїми компромісами.
Run-time через Module Federation (Webpack 5) - поточний стандарт у продакшені. webpack.config.js shell-застосунку оголошує віддалені MF за URL; кожен remote виставляє компоненти назовні. Спільні залежності (React, react-dom) завантажуються один раз як синглтони. Команди деплоять незалежно, shell підхоплює зміни при наступному завантаженні сторінки.
Run-time через single-spa - оркестратор без прив'язки до фреймворку. Кожен MF реєструє lifecycle (bootstrap/mount/unmount) і функцію активності, яка повідомляє оркестратору коли його показувати. Підходить для різнорідних стеків.
iframes дають повну ізоляцію і прості в налаштуванні, але незручні в роботі. Крос-оригінова комунікація потребує postMessage, з маршрутизацією багато складнощів, і accessibility страждає. Варто використовувати лише там, де справді потрібна повна пісочниця - наприклад, для вбудовування стороннього контенту.
Інтеграція під час збірки (npm-пакети) означає, що MF публікуються як пакети і імпортуються в момент збірки. При цьому втрачається незалежність деплою: щоб оновити один MF, треба перезібрати і перевикотити host. Рідко те, що насправді потрібно.
Коли використовувати
Мікрофронтенди варто використовувати коли:
- Є 3+ команди, яким треба викочувати зміни без координації між собою
- Додаток має чітко розділені бізнес-домени (каталог, оформлення замовлення, акаунт)
- Відбувається міграція з legacy-моноліту і треба замінювати по одному розділу
- Команди принципово використовують різні стеки і готові прийняти цю складність
Мікрофронтенди зайві коли:
- Команда менше 20 розробників
- Будується MVP або стартаповий продукт
- Додаток переважно статичний або контентний
- Добре структурований монорепозиторій з чіткими межами модулів дасть 80% незалежності за 20% витрат
Як браузер компонує мікрофронтенди
Браузер завантажує HTML shell-застосунку. SystemJS (або нативні ES-модулі) обробляє динамічні імпорти через System.import(). Кожен MF-бандл реєструє lifecycle в оркестраторі. Під час зміни маршруту оркестратор перевіряє window.location, викликає mount() на активних MF і unmount() на неактивних. CSS-ізоляція реалізується через Shadow DOM (element.attachShadow({ mode: 'open' })) або CSS Modules. Комунікація між MF - через Custom Events на window або BroadcastChannel для крос-оригінових сценаріїв.
На практиці команди стабільно недооцінюють lifecycle розмонтування. Пропустиш його - і при кожній навігації накопичуються витоки пам'яті від event listeners, що особливо помітно в довгих SPA-сесіях.
Поширені помилки
Витоки глобальних CSS-стилів між MF. Одна команда додає .btn { color: red !important } у свій глобальний CSS, і всі кнопки в кожному іншому MF стають червоними.
/* Неправильно: без обмеження області дії, каскадується всюди */
.btn { color: red !important; } /* Ламає кожен сусідній MF */
/* Правильно: CSS Modules обмежує стилі компонентом */
/* MF1.module.css */
.btn { color: blue; } /* Застосовується тільки всередині MF1 */CSS Modules або Shadow DOM вирішують цю проблему. Shadow DOM суворіший, але потребує більше налаштувань.
React завантажується п'ять разів. Без конфігурації синглтона в Module Federation кожен MF пакує власну копію React. П'ять MF - це приблизно 5 x 130кб зайвого трафіку.
// webpack.config.js - обов'язково для спільних залежностей
new ModuleFederationPlugin({
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})Тісна зв'язаність через змінні на window. Команди використовують window.userState = {} як спільне сховище. Два MF перезаписують його під час монтування - виникає race condition. Використовуй натомість Custom Events або BroadcastChannel.
// Неправильно: спільний мутабельний глобальний об'єкт
window.userState = { userId: 123 }; // Перезапишеться наступним MF
// Правильно: комунікація через події
window.dispatchEvent(new CustomEvent('user-login', { detail: { userId: 123 } }));
// Для крос-оригінових MF (iframe або окремі домени):
const channel = new BroadcastChannel('mf-channel');
channel.postMessage({ type: 'user-login', userId: 123 });Пряма маніпуляція з DOM іншого MF. Коли MF A робить document.getElementById('mf-b-root').innerHTML = '', це ламає lifecycle розмонтування MF B і призводить до помилок фреймворку про вже розмонтовані дерева. Ніколи не чіпай DOM іншого MF напряму.
Пропуск lifecycle розмонтування. Якщо MF не прибирає event listeners і таймери під час розмонтування, при кожній навігації накопичуються витоки. Завжди реалізовуй unmount у single-spa або повертай cleanup-функцію в Module Federation.
Де зустрічається в реальних проектах
- Netflix: 100+ MF через single-spa; команда магазинів деплоїть щотижня, не торкаючись движка рекомендацій
- IKEA: Module Federation з окремими MF для лендингу, tradfri і каталогу в різних репозиторіях
- Spotify: Webpack Module Federation; MF подкастів і плеєра спільно використовують синглтон RxJS
- Zalando: 200+ MF на фреймворку Luigi, суміш Angular і React між командами
- Зазвичай до мікрофронтендів приходять тоді, коли крос-командні деплої відбуваються частіше ніж раз на тиждень і вартість координації стає видимою під час планування
Питання на співбесіді
Q: Як керувати спільними залежностями на кшталт React між MF?
A: Вирівнюй версії через синглтони Module Federation: shared: { react: { singleton: true, strictVersion: true } }. Якщо версії розходяться, завантажується другий екземпляр з CDN - це сигнал виправити невідповідність.
Q: Який вплив на продуктивність від runtime-композиції?
A: При першому завантаженні додається приблизно 200-500 мс на додаткові запити до JS-бандлів. Частково вирішується через <link rel="modulepreload"> для передбачуваних MF і lazy-завантаженням для маршрутів, які користувач ще не відвідував.
Q: Як синхронізувати стан між MF без глобального Redux store?
A: BroadcastChannel підходить для слабко пов'язаних подій між MF, включаючи iframe-сценарії. URL-параметри зберігають навігаційний стан. Для тіснішої синхронізації (наприклад, лічильник кошика у хедері) підійде невеликий спільний модуль стану через Module Federation - але тримай його мінімальним.
Q: Shadow DOM чи CSS-in-JS для ізоляції стилів?
A: Shadow DOM дає повну ізоляцію через constructable stylesheets (Chrome 89+). CSS-in-JS (Styled Components, Emotion) дає обмежені стилі з менш суворими межами. Shadow DOM безпечніший на межах між різними фреймворками; CSS-in-JS простіший, якщо всі MF використовують один фреймворк.
Q: (Senior) Як мігрувати з моноліту на мікрофронтенди без даунтайму?
A: Проксуй legacy-маршрут /old на моноліт через iframe або reverse proxy. Розгорни новий MF за feature flag. Використовуй import maps у single-spa для заміни URL модуля в runtime: "legacy-app" вказує сьогодні на бандл моноліту, після перемикання - на бандл нового MF. Відкочування - просто оновлення import map без редеплою.
Q: Як SSR працює з мікрофронтендами?
A: Shell серверно рендерить свій статичний каркас і карту гідратації. Кожен MF серверно рендерить свій фрагмент, який зшивається через Edge-Side Includes (ESI) на рівні CDN. Podium - фреймворк для цього патерну. Більшість команд починають з CSR і додають SSR для окремих MF тільки там, де важливі SEO або FCP.
Приклади
Module Federation: shell завантажує Vue-кошик у React-список продуктів
// shell/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// Cart MF живе за власною URL-адресою
cart: 'cart@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};// shell/src/ProductList.jsx
import React, { lazy, Suspense } from 'react';
// Завантажується з віддаленого Vue MF під час виконання
const Cart = lazy(() => import('cart/CartWidget'));
export default function ProductList() {
return (
<div>
<h2>Продукти</h2>
<Suspense fallback={<div>Завантаження кошика...</div>}>
<Cart /> {/* Vue-компонент рендериться всередині React-дерева */}
</Suspense>
</div>
);
}Коли користувач відкриває /products, shell завантажує remoteEntry.js з localhost:3002, отримує бандл кошика і React рендерить його через Suspense. Команда Vue деплоїть нову версію кошика - користувачі побачать її при наступному завантаженні сторінки без редеплою shell.
Комунікація між MF через BroadcastChannel
Custom Events працюють для MF на одному домені. Для крос-оригінових сценаріїв або iframe-застосунків BroadcastChannel - правильний інструмент.
// Navbar MF (React) - користувач логіниться, сповіщає решту MF
function Navbar() {
const handleLogin = () => {
const channel = new BroadcastChannel('mf-events');
channel.postMessage({ type: 'user-login', userId: 123 });
};
return <button onClick={handleLogin}>Увійти</button>;
}
// Dashboard MF (Vue) - отримує подію
// У mounted() або setup()
const channel = new BroadcastChannel('mf-events');
channel.addEventListener('message', (event) => {
if (event.data.type === 'user-login') {
console.log('Користувач увійшов:', event.data.userId);
// Оновлюємо локальний стан
}
});
// Закриваємо канал при розмонтуванні MF
channel.close();BroadcastChannel працює між окремими контекстами браузера на одному origin. Для iframe з різними origin потрібен postMessage з явним targetOrigin.
Міграція legacy: старий Angular-застосунок поряд з новим React MF
// single-spa root-config.js
import { registerApplication, start } from 'single-spa';
// Legacy-секція (Angular, ще не замінена)
registerApplication({
name: 'legacy-checkout',
app: () => System.import('http://legacy.internal/checkout.js'),
activeWhen: ['/checkout'],
});
// Новий React MF - той самий маршрут, за feature flag
registerApplication({
name: 'new-checkout',
app: () => System.import('http://mf-checkout.internal/remoteEntry.js'),
activeWhen: ['/checkout'],
customProps: {
enabled: window.__FLAGS__?.newCheckout ?? false,
},
});
start();Feature flag перемикає трафік між legacy і новим MF. Коли новий MF проходить QA, прапор перемикається. Обидва застосунки співіснують під час міграції. Angular-команда продовжує своє, React-команда будує поряд без спільного деплой-пайплайну.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.