microservices
Стратегии проектирования микрофронтендов
Подходы к построению микрофронтенд архитектуры для масштабируемых веб-приложений
•
#microservices
#microfrontends
#frontend
#architecture
Стратегии проектирования микрофронтендов
Микрофронтенды — это архитектурный подход, при котором фронтенд-приложение разбивается на независимые части, разрабатываемые и развертываемые отдельными командами.
Основные подходы
1. Build-time Integration
Интеграция на этапе сборки через npm пакеты:
// package.json
{
"dependencies": {
"@company/header-mfe": "^1.2.0",
"@company/products-mfe": "^2.0.1",
"@company/checkout-mfe": "^1.5.3"
}
}
// App.tsx
import Header from '@company/header-mfe';
import Products from '@company/products-mfe';
import Checkout from '@company/checkout-mfe';
function App() {
return (
<div>
<Header />
<Products />
<Checkout />
</div>
);
}
Плюсы: Простота, оптимизация бандла
Минусы: Требуется пересборка при обновлении MFE
2. Run-time Integration
Server-side Composition (SSI)
# nginx конфигурация
location / {
ssi on;
ssi_types text/html;
}
# index.html
<!DOCTYPE html>
<html>
<body>
<!--#include virtual="/header-service/render" -->
<main id="content">
<!--#include virtual="/products-service/render" -->
</main>
<!--#include virtual="/footer-service/render" -->
</body>
</html>
Client-side Composition
// Shell приложение
class MicroFrontendLoader {
async loadMicroFrontend(name: string, containerId: string) {
const config = await this.getConfig(name);
// Загружаем скрипты
await this.loadScript(config.scriptUrl);
// Монтируем MFE
const container = document.getElementById(containerId);
window[`render${name}`](container, {
basePath: config.basePath,
apiUrl: config.apiUrl
});
}
private loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => resolve();
script.onerror = reject;
document.head.appendChild(script);
});
}
}
3. Module Federation (Webpack 5)
// host/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// products/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/ProductList',
'./ProductDetail': './src/ProductDetail'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// Использование в host
import React, { lazy, Suspense } from 'react';
const ProductList = lazy(() => import('products/ProductList'));
const Checkout = lazy(() => import('checkout/CheckoutFlow'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProductList />
<Checkout />
</Suspense>
);
}
Коммуникация между MFE
1. Custom Events
// Отправка события из MFE
class ProductMFE {
addToCart(product: Product) {
const event = new CustomEvent('product:added', {
detail: { product, timestamp: Date.now() }
});
window.dispatchEvent(event);
}
}
// Прослушивание в другом MFE
class CartMFE {
constructor() {
window.addEventListener('product:added', this.handleProductAdded);
}
handleProductAdded = (event: CustomEvent) => {
const { product } = event.detail;
this.updateCart(product);
}
}
2. Shared State Management
// Общее хранилище
class SharedStore {
private state: Map<string, any> = new Map();
private subscribers: Map<string, Set<Function>> = new Map();
setState(key: string, value: any) {
this.state.set(key, value);
this.notify(key, value);
}
getState(key: string) {
return this.state.get(key);
}
subscribe(key: string, callback: Function) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set());
}
this.subscribers.get(key)!.add(callback);
return () => this.subscribers.get(key)?.delete(callback);
}
private notify(key: string, value: any) {
this.subscribers.get(key)?.forEach(callback => callback(value));
}
}
// Использование
const store = new SharedStore();
// В Products MFE
store.subscribe('cart', (cart) => {
console.log('Cart updated:', cart);
});
// В Cart MFE
store.setState('cart', { items: [...], total: 100 });
3. Props и Callbacks
// Shell передает props в MFE
interface MicroFrontendProps {
user: User;
onNavigate: (path: string) => void;
apiClient: ApiClient;
}
function renderProductsMFE(container: HTMLElement, props: MicroFrontendProps) {
ReactDOM.render(
<ProductsApp {...props} />,
container
);
}
Маршрутизация
Централизованная маршрутизация
class ShellRouter {
private routes: Map<string, MicroFrontend> = new Map([
['/products', { name: 'products', container: '#mfe-products' }],
['/checkout', { name: 'checkout', container: '#mfe-checkout' }],
['/profile', { name: 'profile', container: '#mfe-profile' }]
]);
constructor(private loader: MicroFrontendLoader) {
this.setupRouting();
}
private setupRouting() {
window.addEventListener('popstate', () => this.handleRoute());
this.handleRoute();
}
private async handleRoute() {
const path = window.location.pathname;
const mfe = this.findMatchingMFE(path);
if (mfe) {
await this.loader.loadMicroFrontend(mfe.name, mfe.container);
}
}
}
Децентрализованная маршрутизация
// Каждый MFE управляет своими роутами
class ProductsMFE {
private router: Router;
constructor(basePath: string) {
this.router = new Router({ basePath });
this.setupRoutes();
}
private setupRoutes() {
this.router.on('/list', () => this.renderList());
this.router.on('/detail/:id', (params) => this.renderDetail(params.id));
this.router.on('/search', () => this.renderSearch());
}
}
Стилизация и изоляция
CSS Modules
// ProductCard.module.css
.card {
border: 1px solid #ddd;
padding: 1rem;
}
// ProductCard.tsx
import styles from './ProductCard.module.css';
export function ProductCard() {
return <div className={styles.card}>...</div>;
}
Shadow DOM
class ProductWidget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot!.innerHTML = `
<style>
:host {
display: block;
padding: 1rem;
}
.product { /* Изолированные стили */ }
</style>
<div class="product">...</div>
`;
}
}
customElements.define('product-widget', ProductWidget);
CSS-in-JS с префиксами
import styled from 'styled-components';
// Автоматическая генерация уникальных классов
const ProductCard = styled.div`
border: 1px solid #ddd;
padding: 1rem;
`;
Управление зависимостями
Shared Dependencies
// webpack.config.js
new ModuleFederationPlugin({
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
strictVersion: false
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0'
},
'@company/ui-kit': {
singleton: true,
requiredVersion: '^2.0.0'
}
}
})
Version Management
class DependencyManager {
checkCompatibility(mfe: string, version: string): boolean {
const requirements = this.getRequirements(mfe);
return this.satisfiesVersion(version, requirements);
}
private satisfiesVersion(version: string, requirement: string): boolean {
// Проверка совместимости версий
return semver.satisfies(version, requirement);
}
}
Best Practices
- Независимое развертывание — каждый MFE должен деплоиться отдельно
- Слабая связанность — минимизируйте зависимости между MFE
- Единый дизайн — используйте общую UI библиотеку
- Производительность — оптимизируйте загрузку и размер бандлов
- Мониторинг — отслеживайте ошибки в каждом MFE отдельно
Заключение
Микрофронтенды позволяют масштабировать фронтенд-разработку, но требуют тщательного проектирования коммуникации, маршрутизации и управления зависимостями.