Skip to main content

Архітектура мікрофронтендів

Архітектура мікрофронтендів розбиває фронтенд-застосунок на кілька незалежних веб-застосунків, кожен з яких належить окремій команді і завантажується в єдиний UI під час виконання.

Теорія

TL;DR

  • Аналогія: як конструктор Lego - кожна команда будує і випускає свій блок незалежно; shell-застосунок збирає їх разом без перебудови всього проекту
  • Головна відмінність від моноліту: команди деплоять окремо, обирають власні фреймворки (React у команди A, Vue у команди B), і зламаний CSS однієї команди не руйнує інтерфейс іншої
  • Module Federation (Webpack 5) - домінуючий підхід у продакшені; single-spa - інший популярний варіант
  • Варто використовувати якщо 50+ розробників у кількох командах; для команди до 20 людей зайве
  • Реальна ціна - інфраструктура: кожен MF потребує власного CI/CD, моніторингу та версіонування

Швидкий приклад

Shell-застосунок через single-spa завантажує два мікрофронтенди під час виконання:

html
<!-- 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 стають червоними.

css
/* Неправильно: без обмеження області дії, каскадується всюди */ .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кб зайвого трафіку.

javascript
// 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.

javascript
// Неправильно: спільний мутабельний глобальний об'єкт 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-список продуктів

javascript
// 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 } } }) ] };
jsx
// 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 - правильний інструмент.

javascript
// 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

javascript
// 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-команда будує поряд без спільного деплой-пайплайну.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?