Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Micro-frontends z React - architektura skalowalna

Micro-frontends to architekturowy wzorzec, który rozszerza koncepcję mikroserwisów na frontend, umożliwiając niezależnym zespołom rozwijanie, wdrażanie i skalowanie poszczególnych części aplikacji frontendowej.

Wprowadzenie do Micro-frontends

Czym są Micro-frontends?

Micro-frontends to technika dzielenia frontendowej aplikacji monolitycznej na mniejsze, niezależne aplikacje, które mogą być rozwijane przez różne zespoły i wdrażane niezależnie.

Module Federation (Webpack 5)

Podstawowa konfiguracja

1// webpack.config.js - Shell Application
2const ModuleFederationPlugin = require('@module-federation/webpack');
3
4module.exports = {
5  mode: 'development',
6  devServer: {
7    port: 3000,
8  },
9  plugins: [
10    new ModuleFederationPlugin({
11      name: 'shell',
12      remotes: {
13        products: 'products@http://localhost:3001/remoteEntry.js',
14        cart: 'cart@http://localhost:3002/remoteEntry.js',
15        user: 'user@http://localhost:3003/remoteEntry.js',
16      },
17      shared: {
18        react: { singleton: true, eager: true },
19        'react-dom': { singleton: true, eager: true },
20        '@shared/ui-kit': { singleton: true },
21        '@shared/utils': { singleton: true },
22      },
23    }),
24  ],
25};

Implementacja Shell Application

1// src/App.jsx - Shell Application
2import React, { Suspense, lazy } from 'react';
3import { BrowserRouter, Routes, Route } from 'react-router-dom';
4import ErrorBoundary from './components/ErrorBoundary';
5import Layout from './components/Layout';
6
7// Lazy loading Micro-frontends
8const ProductsApp = lazy(() => import('products/ProductsApp'));
9const CartApp = lazy(() => import('cart/CartApp'));
10const UserApp = lazy(() => import('user/UserApp'));
11
12// Local components
13const HomePage = lazy(() => import('./pages/HomePage'));
14
15function App() {
16  return (
17    <ErrorBoundary>
18      <BrowserRouter>
19        <Layout>
20          <Suspense fallback={<div>Loading...</div>}>
21            <Routes>
22              <Route path="/" element={<HomePage />} />
23
24              {/* Products Micro-frontend */}
25              <Route
26                path="/products/*"
27                element={
28                  <ErrorBoundary fallback={<div>Products unavailable</div>}>
29                    <ProductsApp />
30                  </ErrorBoundary>
31                }
32              />
33
34              {/* Cart Micro-frontend */}
35              <Route
36                path="/cart/*"
37                element={
38                  <ErrorBoundary fallback={<div>Cart unavailable</div>}>
39                    <CartApp />
40                  </ErrorBoundary>
41                }
42              />
43
44              {/* User Micro-frontend */}
45              <Route
46                path="/user/*"
47                element={
48                  <ErrorBoundary fallback={<div>User profile unavailable</div>}>
49                    <UserApp />
50                  </ErrorBoundary>
51                }
52              />
53            </Routes>
54          </Suspense>
55        </Layout>
56      </BrowserRouter>
57    </ErrorBoundary>
58  );
59}
60
61export default App;

Komunikacja między Micro-frontends

Event-driven Architecture

1// shared/eventBus.js
2class EventBus {
3  constructor() {
4    this.events = {};
5  }
6
7  subscribe(eventName, callback) {
8    if (!this.events[eventName]) {
9      this.events[eventName] = [];
10    }
11    this.events[eventName].push(callback);
12
13    // Return unsubscribe function
14    return () => {
15      this.events[eventName] = this.events[eventName].filter(
16        cb => cb !== callback
17      );
18    };
19  }
20
21  emit(eventName, data) {
22    if (this.events[eventName]) {
23      this.events[eventName].forEach(callback => callback(data));
24    }
25
26    // Also emit as CustomEvent for cross-MFE communication
27    window.dispatchEvent(new CustomEvent(`mfe:${eventName}`, {
28      detail: data
29    }));
30  }
31}
32
33export const eventBus = new EventBus();

Shared State Management

1// shared/stateManager.js
2import { createStore } from 'zustand/vanilla';
3
4// Global store accessible across MFEs
5const globalStore = createStore((set, get) => ({
6  // User state
7  user: null,
8  isAuthenticated: false,
9
10  // Cart state
11  cartItems: [],
12  cartTotal: 0,
13
14  // Actions
15  setUser: (user) => set({ user, isAuthenticated: !!user }),
16
17  addToCart: (product) => set(state => {
18    const existingItem = state.cartItems.find(item => item.id === product.id);
19    let newItems;
20
21    if (existingItem) {
22      newItems = state.cartItems.map(item =>
23        item.id === product.id
24          ? { ...item, quantity: item.quantity + 1 }
25          : item
26      );
27    } else {
28      newItems = [...state.cartItems, { ...product, quantity: 1 }];
29    }
30
31    const newTotal = newItems.reduce((sum, item) =>
32      sum + (item.price * item.quantity), 0
33    );
34
35    return { cartItems: newItems, cartTotal: newTotal };
36  }),
37
38  removeFromCart: (productId) => set(state => {
39    const newItems = state.cartItems.filter(item => item.id !== productId);
40    const newTotal = newItems.reduce((sum, item) =>
41      sum + (item.price * item.quantity), 0
42    );
43
44    return { cartItems: newItems, cartTotal: newTotal };
45  }),
46}));

Testing Micro-frontends

1// __tests__/integration/mfe-integration.test.jsx
2import { render, screen, waitFor } from '@testing-library/react';
3import userEvent from '@testing-library/user-event';
4import { BrowserRouter } from 'react-router-dom';
5import App from '../App';
6
7// Mock MFEs
8jest.mock('products/ProductsApp', () => {
9  return function MockProductsApp() {
10    return <div data-testid="products-mfe">Products MFE Loaded</div>;
11  };
12});
13
14describe('MFE Integration', () => {
15  test('loads products MFE when navigating to /products', async () => {
16    const user = userEvent.setup();
17
18    render(
19      <BrowserRouter>
20        <App />
21      </BrowserRouter>
22    );
23
24    // Navigate to products
25    await user.click(screen.getByRole('link', { name: /products/i }));
26
27    // Verify MFE is loaded
28    await waitFor(() => {
29      expect(screen.getByTestId('products-mfe')).toBeInTheDocument();
30    });
31  });
32});

Micro-frontends oferują potężne możliwości skalowania aplikacji React w dużych organizacjach, ale wymagają przemyślanej architektury i dobrego zarządzania złożonością.

Vai a CodeWorlds