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

  1. Независимое развертывание — каждый MFE должен деплоиться отдельно
  2. Слабая связанность — минимизируйте зависимости между MFE
  3. Единый дизайн — используйте общую UI библиотеку
  4. Производительность — оптимизируйте загрузку и размер бандлов
  5. Мониторинг — отслеживайте ошибки в каждом MFE отдельно

Заключение

Микрофронтенды позволяют масштабировать фронтенд-разработку, но требуют тщательного проектирования коммуникации, маршрутизации и управления зависимостями.