Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Архітектура мікрофронтендів». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Архітектура мікрофронтендів** розбиває фронтенд-застосунок на незалежні мікро-застосунки, кожен з яких належить окремій команді і збирається в єдиний UI під час виконання. Основні інструменти: Module Federation (Webpack 5) і single-spa. Варто використовувати коли 3+ команди деплоять незалежно; для команди до 20 людей надлишкова.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Архітектура мікрофронтендів** розбиває фронтенд-застосунок на кілька незалежних веб-застосунків, кожен з яких належить окремій команді і завантажується в єдиний 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-команда будує поряд без спільного деплой-пайплайну.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.