Микрофронтенды: архитектура для больших команд
Module Federation, Single-SPA и независимая разработка frontend приложений
Монолитный фронтенд становится бутылочным горлышком в больших командах. Конфликты в git, долгие билды, сложность деплоя. Микрофронтенды решают эти проблемы, позволяя командам работать независимо. Что такое микрофронтенды Разделение фронтенд приложения на независимые, самостоятельно деплоящиеся части. Преимущества: Независимость команд — каждая команда владеет своей частью Независимый деплой — обновление без перезапуска всего Технологическая свобода — разные фреймворки в одном приложении Инкрементальные обновления — постепенная миграция Недостатки: Сложность интеграции Дублирование зависимостей Performance overhead Сложность shared state Подходы к микрофронтендам 1. iframe интеграция Самый простой, но ограниченный подход: Плюсы: полная изоляция, простота Минусы: сложность коммуникации, SEO, performance 2. JavaScript интеграция Загрузка микрофронтендов как JavaScript модулей: // Host application const script = document.createElement('script'); script.src = 'https://team-a.example.com/bundle.js'; script.onload = () => { window.teamAApp.mount('#team-a-root'); }; document.body.appendChild(script); 3. Web Components Использование Custom Elements: // Team A микрофронтенд class ProductList extends HTMLElement { connectedCallback() { this.innerHTML = ` Products `; } } customElements.define('product-list', ProductList); // Host application Webpack Module Federation Современный подход для sharing кода между приложениями. Host application (webpack.config.js): const { ModuleFederationPlugin } = require('webpack').container; module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'host', remotes: { teamA: 'teamA@http://localhost:3001/remoteEntry.js', teamB: 'teamB@http://localhost:3002/remoteEntry.js', }, shared: { react: { singleton: true, eager: true }, 'react-dom': { singleton: true, eager: true }, }, }), ], }; Remote application (Team A): new ModuleFederationPlugin({ name: 'teamA', filename: 'remoteEntry.js', exposes: { './ProductList': './src/ProductList', './ProductDetail': './src/ProductDetail', }, shared: { react: { singleton: true }, 'react-dom': { singleton: true }, }, }) Использование в host: // Динамический импорт const ProductList = React.lazy(() => import('teamA/ProductList')); function App() { return ( Loading... }> ); } Single-SPA Framework-agnostic роутинг для микрофронтендов. Root config: import { registerApplication, start } from 'single-spa'; registerApplication({ name: '@org/navbar', app: () => System.import('@org/navbar'), activeWhen: '/', }); registerApplication({ name: '@org/products', app: () => System.import('@org/products'), activeWhen: '/products', }); registerApplication({ name: '@org/checkout', app: () => System.import('@org/checkout'), activeWhen: '/checkout', }); start(); Микрофронтенд (React): import React from 'react'; import ReactDOM from 'react-dom'; import singleSpaReact from 'single-spa-react'; import Root from './root.component'; const lifecycles = singleSpaReact({ React, ReactDOM, rootComponent: Root, errorBoundary(err, info, props) { return Error loading micro-frontend ; }, }); export const { bootstrap, mount, unmount } = lifecycles; Shared State Management 1. Custom Events: // Team A публикует событие window.dispatchEvent(new CustomEvent('product:added', { detail: { productId: 123, quantity: 1 } })); // Team B слушает событие window.addEventListener('product:added', (event) => { updateCart(event.detail); }); 2. Shared State Store: // Shared store (Zustand) import create from 'zustand'; export const useSharedStore = create((set) => ({ user: null, cart: [], setUser: (user) => set({ user }), addToCart: (product) => set((state) => ({ cart: [...state.cart, product] })), })); // Использование в микрофронтендах import { useSharedStore } from '@shared/store'; function ProductList() { const addToCart = useSharedStore(state => state.addToCart); // ... } 3. Props drilling через root: // Root передает props Routing Координация роутинга: // Shared routing package export const navigate = (path) => { window.history.pushState(null, null, path); window.dispatchEvent(new PopStateEvent('popstate')); }; // В микрофронтендах import { navigate } from '@shared/routing'; function ProductCard({ product }) { return ( navigate(`/products/${product.id}`)}> {product.name} ); } Styling изоляция 1. CSS Modules: // TeamA.module.css .container { /* styles */ } // Component import styles from './TeamA.module.css'; function TeamA() { return ... ; } 2. CSS-in-JS: import styled from 'styled-components'; const Container = styled.div` /* Автоматическая изоляция */ padding: 1rem; `; 3. Shadow DOM: class TeamAComponent extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` /* Полностью изолированные стили */ .container { padding: 1rem; } ... `; } } Performance оптимизация 1. Shared dependencies: // Module Federation shared shared: { react: { singleton: true, eager: true }, 'react-dom': { singleton: true, eager: true }, lodash: { singleton: true }, } 2. Lazy loading: const TeamA = React.lazy(() => import('teamA/App')); function App() { return ( }> ); } 3. Preloading: // Preload критичных микрофронтендов const preloadTeamA = () => import('teamA/App'); // Preload на hover Products Testing 1. Unit tests: Каждый микрофронтенд тестируется независимо. 2. Integration tests: // Тест интеграции между микрофронтендами describe('Product to Cart integration', () => { it('should add product to cart', async () => { render( ); // Team A: клик на продукт const addButton = screen.getByText('Add to Cart'); fireEvent.click(addButton); // Team B: проверка корзины const cartCount = await screen.findByTestId('cart-count'); expect(cartCount).toHaveTextContent('1'); }); }); 3. E2E tests: Полный user flow через все микрофронтенды. Deployment стратегии 1. Independent deployment: # Deploy Team A cd team-a npm run build aws s3 sync dist/ s3://team-a-bucket/ # Deploy Team B независимо cd team-b npm run build aws s3 sync dist/ s3://team-b-bucket/ 2. Version management: // Import map для версионирования { "imports": { "teamA": "https://cdn.example.com/team-a@1.2.3/bundle.js", "teamB": "https://cdn.example.com/team-b@2.0.1/bundle.js" } } Когда НЕ использовать микрофронтенды Маленькая команда ( Простое приложение Нет необходимости в независимом деплое Одна команда владеет всем фронтендом Альтернативы: монорепозиторий с package-based разделением Best Practices Vertical slicing — каждая команда владеет full-stack feature API contracts — четкие интерфейсы между микрофронтендами Shared design system — консистентный UX Central routing — координация навигации Error boundaries — изоляция ошибок Monitoring — отслеживание performance каждого микрофронтенда Заключение: Микрофронтенды — мощный паттерн для больших команд и сложных приложений. Используйте Module Federation для modern setup. Начните с Simple-SPA для framework-agnostic подхода. Инвестируйте в shared dependencies и design system. Помните: микрофронтенды добавляют сложность, используйте только когда масштаб требует.