İçeriğe atla

Micro Frontend Uygulama Pattern'leri: Pratik Rehber

Module Federation kurulumu, micro frontend'ler arası iletişim, routing stratejileri ve bölünmüş routing'i yük altında bozan yarış durumları.

Ayhan Sipahi Ayhan Sipahi

Micro Frontend Serisi Navigasyonu#

  • Bölüm 1: Mimari temelleri ve uygulama türleri
  • Bölüm 2 (Şu anda buradasınız): Module Federation, iletişim pattern’leri ve entegrasyon stratejileri
  • Bölüm 3: İleri düzey pattern’ler, performans optimizasyonu ve production hata ayıklama

Ön Koşullar: Aşağıdaki bölümler Bölüm 1’deki kavramları veri kabul eder. Micro frontend’lere yeniyseniz, önce oradan başlayın.


Bölüm 1 micro frontend’ler için temel mimari pattern’leri ele aldı. Module Federation, runtime entegrasyonunda baskın yaklaşımdır ve asıl pratik iş onun kurulumunda, etrafındaki iletişim pattern’lerinde ve production sistemlerinin gerektirdiği hata ayıklama stratejilerinde yatar.

Module Federation’a Yakından Bakış#

Webpack 5 ile gelen Module Federation, runtime entegrasyon işinin büyük bölümünü üstleniyor. Dinamik import’ların verdiğinin ötesinde bağımlılık paylaşımı, versiyon uzlaşımı ve runtime kompozisyon sunar.

Production’a Hazır Module Federation Kurulumu#

Farklı takımların farklı domain’lere sahip olduğu gerçekçi bir e-ticaret uygulaması oluşturalım:

// apps/shell/webpack.config.js
// Not: Yeni özellikler yerine production güvenilirliği için stabil versiyonlar kullanıyoruz.
// @module-federation/enhanced, Module Federation 2.0'ı sağlar; webpack 5'in yerleşik
// plugin'i hâlâ webpack.container.ModuleFederationPlugin altında
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
    ],
  },

  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      filename: 'remoteEntry.js',

      remotes: {
        // Ürün takımının micro frontend'i
        products: 'products@http://localhost:3001/remoteEntry.js',
        // Sepet takımının micro frontend'i
        cart: 'cart@http://localhost:3002/remoteEntry.js',
        // Kullanıcı takımının micro frontend'i
        user: 'user@http://localhost:3003/remoteEntry.js',
      },

      shared: {
        // Production'da kanıtlanmış stabil versiyonlar kullanıyoruz - her zaman en yenisi değil
        // Bu tüm micro frontend'lerde uyumluluğu garanti ediyor
        react: {
          singleton: true,
          strictVersion: true,
          requiredVersion: '^18.2.0',
        },
        'react-dom': {
          singleton: true,
          strictVersion: true,
          requiredVersion: '^18.2.0',
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: '^6.8.0',
        },
        // Özel paylaşılan yardımcılar
        '@company/design-system': {
          singleton: true,
          requiredVersion: '^2.1.0',
        },
        '@company/event-bus': {
          singleton: true,
          requiredVersion: '^1.0.0',
        }
      },
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],

  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },

  devServer: {
    port: 3000,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
};
// apps/products/webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',

  plugins: [
    new ModuleFederationPlugin({
      name: 'products',
      filename: 'remoteEntry.js',

      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
        './ProductSearch': './src/components/ProductSearch',
      },

      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.2.0',
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: '^6.8.0',
        },
        '@company/design-system': {
          singleton: true,
          requiredVersion: '^2.1.0',
        },
        '@company/event-bus': {
          singleton: true,
          requiredVersion: '^1.0.0',
        }
      },
    }),
  ],

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },

  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },

  devServer: {
    port: 3001,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
};

Error Boundary’lerle Dayanıklı Modül Yükleme#

Module Federation’da en büyük zorluklardan biri yükleme hatalarını zarif bir şekilde ele almaktır. Aşağıdaki loader, yüklenemeyen bir remote’un sayfanın geri kalanını da götürmesini engeller:

// src/components/MicroFrontendLoader.tsx
import React, { Suspense, lazy, useState, useEffect } from 'react';

interface MicroFrontendConfig {
  scope: string;
  module: string;
  url: string;
  fallback?: React.ComponentType;
}

interface LoadingState {
  isLoading: boolean;
  error: Error | null;
  retryCount: number;
}

const useDynamicScript = (url: string) => {
  const [ready, setReady] = useState(false);
  const [failed, setFailed] = useState(false);

  useEffect(() => {
    if (!url) return;

    const element = document.createElement('script');
    element.src = url;
    element.type = 'text/javascript';
    element.async = true;

    setReady(false);
    setFailed(false);

    element.onload = () => {
      console.log(`Dinamik Script Yüklendi: ${url}`);
      setReady(true);
    };

    element.onerror = () => {
      console.error(`Dinamik Script Hatası: ${url}`);
      setReady(false);
      setFailed(true);
    };

    document.head.appendChild(element);

    return () => {
      console.log(`Dinamik Script Kaldırıldı: ${url}`);
      document.head.removeChild(element);
    };
  }, [url]);

  return { ready, failed };
};

const loadComponent = (scope: string, module: string) => {
  return async () => {
    // Paylaşım kapsamını başlatır. Bu, bu build'den ve tüm remote'lardan bilinen sağlanan modüllerle doldurur
    await __webpack_init_sharing__('default');

    const container = (window as any)[scope]; // veya container'ı başka bir yerden alın
    if (!container) {
      throw new Error(`Container '${scope}' bulunamadı`);
    }

    // Container'ı başlat, paylaşılan modüller sağlayabilir
    await container.init(__webpack_share_scopes__.default);

    const factory = await (window as any)[scope].get(module);
    const Module = factory();
    return Module;
  };
};

class MicroFrontendErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ComponentType },
  { hasError: boolean; error: Error | null }
> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: any) {
    console.error('Micro Frontend Hatası:', error, errorInfo);

    // İzleme servisine gönder
    if (typeof window !== 'undefined' && (window as any).analytics) {
      (window as any).analytics.track('Micro Frontend Hatası', {
        error: error.message,
        stack: error.stack,
        componentStack: errorInfo.componentStack,
      });
    }
  }

  render() {
    if (this.state.hasError) {
      const Fallback = this.props.fallback;
      return <Fallback />;
    }

    return this.props.children;
  }
}

export const MicroFrontendLoader: React.FC<{
  config: MicroFrontendConfig;
  props?: Record<string, any>;
}> = ({ config, props = {} }) => {
  const { ready, failed } = useDynamicScript(config.url);
  const [loadingState, setLoadingState] = useState<LoadingState>({
    isLoading: false,
    error: null,
    retryCount: 0,
  });

  useEffect(() => {
    if (ready && !loadingState.isLoading) {
      setLoadingState(prev => ({ ...prev, isLoading: true, error: null }));
    }
  }, [ready]);

  const handleRetry = () => {
    if (loadingState.retryCount < 3) {
      setLoadingState(prev => ({
        ...prev,
        retryCount: prev.retryCount + 1,
        error: null,
        isLoading: true,
      }));

      // Script'i yeniden yüklemeye zorla
      window.location.reload();
    }
  };

  if (failed || (loadingState.error && loadingState.retryCount >= 3)) {
    const Fallback = config.fallback || DefaultErrorFallback;
    return <Fallback onRetry={handleRetry} />;
  }

  if (!ready) {
    return <LoadingFallback />;
  }

  const Component = lazy(loadComponent(config.scope, config.module));

  return (
    <MicroFrontendErrorBoundary fallback={config.fallback || DefaultErrorFallback}>
      <Suspense fallback={<LoadingFallback />}>
        <Component {...props} />
      </Suspense>
    </MicroFrontendErrorBoundary>
  );
};

const LoadingFallback: React.FC = () => (
  <div className="animate-pulse">
    <div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
    <div className="h-4 bg-gray-300 rounded w-1/2"></div>
  </div>
);

const DefaultErrorFallback: React.FC<{ onRetry?: () => void }> = ({ onRetry }) => (
  <div className="p-4 border border-red-300 rounded bg-red-50">
    <h3 className="text-red-800 font-semibold">Bir şeyler yanlış gitti</h3>
    <p className="text-red-600 text-sm mt-1">
      Bu bölüm yüklenemedi. Lütfen sayfayı yenilemeyi deneyin.
    </p>
    {onRetry && (
      <button
        onClick={onRetry}
        className="mt-2 px-3 py-1 bg-red-600 text-white rounded text-sm"
      >
        Tekrar Dene
      </button>
    )}
  </div>
);

Cross-Micro Frontend İletişimi#

Micro frontend mimarisinin en zorlu yönlerinden biri, bağımsız olarak geliştirilmiş ve dağıtılmış uygulamalar arasında iletişimi sağlamaktır. Küçük bir replay buffer’ı olan paylaşılan event bus çoğu durumu karşılar; geç yüklenen remote’ları senkron tutan da bu replay kısmıdır:

1. Event-Driven İletişim#

// @company/event-bus - Paylaşılan event bus paketi
import { useCallback, useEffect } from 'react';

interface EventBusEvent {
  type: string;
  payload: any;
  source: string;
  timestamp: number;
}

class EventBus {
  private listeners: Map<string, Array<(event: EventBusEvent) => void>> = new Map();
  private eventHistory: EventBusEvent[] = [];
  private maxHistorySize = 50;

  subscribe(eventType: string, callback: (event: EventBusEvent) => void): () => void {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }

    this.listeners.get(eventType)!.push(callback);

    // Unsubscribe fonksiyonunu döndür
    return () => {
      const callbacks = this.listeners.get(eventType);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
          callbacks.splice(index, 1);
        }
      }
    };
  }

  publish(type: string, payload: any, source: string = 'unknown') {
    const event: EventBusEvent = {
      type,
      payload,
      source,
      timestamp: Date.now(),
    };

    // Geçmişe ekle
    this.eventHistory.push(event);
    if (this.eventHistory.length > this.maxHistorySize) {
      this.eventHistory.shift();
    }

    // Dinleyicileri bilgilendir
    const callbacks = this.listeners.get(type) || [];
    callbacks.forEach(callback => {
      try {
        callback(event);
      } catch (error) {
        console.error(`${type} için event listener'da hata:`, error);
      }
    });

    // Development'ta debug loglama
    if (process.env.NODE_ENV === 'development') {
      console.log(`[EventBus] ${type}:`, payload);
    }
  }

  getHistory(eventType?: string): EventBusEvent[] {
    if (eventType) {
      return this.eventHistory.filter(event => event.type === eventType);
    }
    return [...this.eventHistory];
  }

  // Geç yüklenen micro frontend'ler için event'leri tekrar oynat
  replayEvents(eventType: string, callback: (event: EventBusEvent) => void) {
    const pastEvents = this.eventHistory.filter(event => event.type === eventType);
    pastEvents.forEach(event => callback(event));
  }
}

export const eventBus = new EventBus();

// Daha kolay kullanım için React hook
export const useEventBus = (
  eventType: string,
  callback: (event: EventBusEvent) => void,
  deps: React.DependencyList = []
) => {
  useEffect(() => {
    const unsubscribe = eventBus.subscribe(eventType, callback);
    return unsubscribe;
  }, deps);

  const publish = useCallback((payload: any, source?: string) => {
    eventBus.publish(eventType, payload, source);
  }, [eventType]);

  return { publish };
};

2. Micro Frontend’lerde Pratik Kullanım#

// products/src/components/ProductList.tsx
import React, { useState, useEffect } from 'react';
import { useEventBus } from '@company/event-bus';

interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
}

export const ProductList: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [filters, setFilters] = useState<any>(null);

  // Arama micro frontend'inden filtre değişikliklerini dinle
  useEventBus('search:filters-changed', (event) => {
    setFilters(event.payload);
  });

  // Geri bildirim göstermek için sepet güncellemelerini dinle
  useEventBus('cart:item-added', (event) => {
    // Başarı bildirimi göster
    showNotification(`${event.payload.productName} sepete eklendi!`);
  });

  const { publish } = useEventBus('products:product-selected', () => {});

  const handleProductClick = (product: Product) => {
    publish({
      productId: product.id,
      productName: product.name,
      source: 'product-list',
    }, 'products');
  };

  useEffect(() => {
    // Değiştiğinde filtreleri uygula
    if (filters) {
      // Ürünleri filtreleme mantığı
      const filtered = applyFilters(products, filters);
      setProducts(filtered);
    }
  }, [filters]);

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
      {products.map(product => (
        <div
          key={product.id}
          className="border rounded-lg p-4 cursor-pointer hover:shadow-lg"
          onClick={() => handleProductClick(product)}
        >
          <img src={product.image} alt={product.name} className="w-full h-48 object-cover" />
          <h3 className="font-semibold mt-2">{product.name}</h3>
          <p className="text-gray-600">${product.price}</p>
        </div>
      ))}
    </div>
  );
};
// cart/src/components/CartButton.tsx
import React, { useState, useEffect } from 'react';
import { useEventBus } from '@company/event-bus';

export const CartButton: React.FC = () => {
  const [itemCount, setItemCount] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);

  // Ürün eklemelerini dinle
  useEventBus('products:product-selected', (event) => {
    addToCart(event.payload.productId);
  });

  const { publish } = useEventBus('cart:item-added', () => {});

  const addToCart = async (productId: string) => {
    try {
      // Sepete ekleme mantığı
      const response = await fetch('/api/cart/add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ productId }),
      });

      if (response.ok) {
        const result = await response.json();
        setItemCount(prev => prev + 1);

        // Animasyonu tetikle
        setIsAnimating(true);
        setTimeout(() => setIsAnimating(false), 500);

        // Diğer micro frontend'leri bilgilendir
        publish({
          productId,
          productName: result.productName,
          newTotal: result.cartTotal,
        }, 'cart');
      }
    } catch (error) {
      console.error('Sepete ekleme başarısız:', error);
    }
  };

  return (
    <button
      className={`relative p-2 bg-blue-600 text-white rounded-full ${
        isAnimating ? 'animate-pulse' : ''
      }`}
    >
      <ShoppingCartIcon className="w-6 h-6" />
      {itemCount > 0 && (
        <span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
          {itemCount}
        </span>
      )}
    </button>
  );
};

Routing Stratejileri#

Micro frontend mimarilerinde routing, çakışmaları önlemek ve sorunsuz bir kullanıcı deneyimi sağlamak için dikkatli koordinasyon gerektirir:

1. Shell Kontrollü Routing#

// shell/src/App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate, Link } from 'react-router-dom';
import { MicroFrontendLoader } from './components/MicroFrontendLoader';

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <div className="min-h-screen bg-gray-50">
        {/* Global navigasyon */}
        <nav className="bg-white shadow-sm border-b">
          <div className="max-w-7xl mx-auto px-4">
            <div className="flex justify-between h-16">
              <div className="flex">
                <Link to="/" className="flex items-center px-4 text-lg font-semibold">
                  E-Ticaret
                </Link>
                <div className="flex space-x-8 ml-8">
                  <Link to="/products" className="flex items-center px-3 py-2 hover:text-blue-600">
                    Ürünler
                  </Link>
                  <Link to="/categories" className="flex items-center px-3 py-2 hover:text-blue-600">
                    Kategoriler
                  </Link>
                </div>
              </div>

              {/* Kullanıcı hesabı micro frontend */}
              <div className="flex items-center">
                <MicroFrontendLoader
                  config={{
                    scope: 'user',
                    module: './UserMenu',
                    url: 'http://localhost:3003/remoteEntry.js',
                  }}
                />
              </div>
            </div>
          </div>
        </nav>

        {/* Ana içerik alanı */}
        <main className="max-w-7xl mx-auto px-4 py-8">
          <Routes>
            <Route path="/" element={<Navigate to="/products" replace />} />

            {/* Products micro frontend tüm /products/* route'larını yönetir */}
            <Route
              path="/products/*"
              element={
                <MicroFrontendLoader
                  config={{
                    scope: 'products',
                    module: './ProductsApp',
                    url: 'http://localhost:3001/remoteEntry.js',
                  }}
                />
              }
            />

            {/* Cart micro frontend */}
            <Route
              path="/cart/*"
              element={
                <MicroFrontendLoader
                  config={{
                    scope: 'cart',
                    module: './CartApp',
                    url: 'http://localhost:3002/remoteEntry.js',
                  }}
                />
              }
            />

            {/* Bilinmeyen route'lar için fallback */}
            <Route path="*" element={<NotFoundPage />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
};

2. Micro Frontend İçinde Routing#

// products/src/ProductsApp.tsx
import React, { useEffect } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { ProductList } from './components/ProductList';
import { ProductDetail } from './components/ProductDetail';
import { ProductSearch } from './components/ProductSearch';

export const ProductsApp: React.FC = () => {
  const location = useLocation();

  // Micro frontend için analytics takibi
  useEffect(() => {
    if (typeof window !== 'undefined' && (window as any).analytics) {
      (window as any).analytics.page('Products', {
        path: location.pathname,
        microfrontend: 'products',
      });
    }
  }, [location.pathname]);

  return (
    <div className="products-app">
      <Routes>
        {/* Not: path'ler /products'a göreceli */}
        <Route index element={<ProductList />} />
        <Route path="search" element={<ProductSearch />} />
        <Route path="category/:categoryId" element={<ProductList />} />
        <Route path=":productId" element={<ProductDetail />} />
      </Routes>
    </div>
  );
};

Route Kaydında Yarış Durumu#

Bölünmüş routing’in dışarıdan rastgele görünen bir hata biçimi var: bir ürün detay sayfası bazı kullanıcılarda, bazen 404 döner ve sayfayı yenilemek sorunu geçirir. Sebep genellikle shell’in router’ı ile route’larını henüz kaydetmemiş remote arasındaki yarıştır.

Bunu tetikleyen kod şu şekilde:

// Sorunlu kod
const ProductsApp: React.FC = () => {
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    // Hazır olma sinyali yerine sabit zamanlayıcı kullanmak yarış penceresini açıyor
    setTimeout(() => setIsLoaded(true), 100);
  }, []);

  if (!isLoaded) {
    return <div>Yükleniyor...</div>;
  }

  return (
    <Routes>
      <Route path=":productId" element={<ProductDetail />} />
    </Routes>
  );
};

Shell’deki React Router, micro frontend kendi route’larını başlatmayı bitirmeden eşleştirmeye çalışır. Gecikme bir hazır olma sinyali değil sabit bir zamanlayıcı olduğu için pencere yükleme zamanlamasına göre açılıp kapanır; 404’lerin rastgele görünmesinin sebebi budur.

Çözüm, hazır olma durumunu açık hale getirmek: remote önce route’larını duyurur, sonra render eder.

// Uygun route kaydı ile düzeltilmiş versiyon
import { useEventBus } from '@company/event-bus';

const ProductsApp: React.FC = () => {
  const [routesReady, setRoutesReady] = useState(false);
  const { publish } = useEventBus('routing:micro-frontend-ready', () => {});

  useEffect(() => {
    // Mevcut route'ları shell'e kaydet
    publish({
      microfrontend: 'products',
      routes: [
        '/products',
        '/products/search',
        '/products/category/:categoryId',
        '/products/:productId'
      ]
    }, 'products');

    setRoutesReady(true);
  }, [publish]);

  if (!routesReady) {
    return <LoadingSpinner />;
  }

  return (
    <Routes>
      <Route index element={<ProductList />} />
      <Route path="search" element={<ProductSearch />} />
      <Route path="category/:categoryId" element={<ProductList />} />
      <Route path=":productId" element={<ProductDetail />} />
    </Routes>
  );
};

Düzeltmeyi kalıcı kılan üç özellik var:

  1. Shell ve micro frontend’ler arasında açık route kaydı
  2. Routing’e müdahale etmeyen uygun yükleme durumları
  3. Production’da route çözümlemenin kapsamlı izlenmesi

Geliştirme ve Test Stratejileri#

Yerel Geliştirme Kurulumu#

// scripts/dev-all.js - Tüm micro frontend'leri yerel olarak çalıştırmak için script
const { spawn } = require('child_process');
const path = require('path');

const services = [
  { name: 'shell', port: 3000, path: './apps/shell' },
  { name: 'products', port: 3001, path: './apps/products' },
  { name: 'cart', port: 3002, path: './apps/cart' },
  { name: 'user', port: 3003, path: './apps/user' },
];

const processes = [];

services.forEach(service => {
  console.log(`${service.name} ${service.port} portunda başlatılıyor...`);

  // Not: buna `process` adını vermeyin - aşağıda okunan global'i gölgeler
  const child = spawn('npm', ['run', 'dev'], {
    cwd: path.resolve(service.path),
    stdio: 'inherit',
    shell: true,
    env: { ...process.env, PORT: service.port.toString() }
  });

  processes.push(child);
});

// Zarif kapanış
process.on('SIGTERM', () => {
  processes.forEach(p => p.kill());
});

process.on('SIGINT', () => {
  processes.forEach(p => p.kill());
  process.exit(0);
});

Entegrasyon Testi#

// tests/integration/micro-frontend-integration.test.ts
import { test, expect } from '@playwright/test';

test.describe('Micro Frontend Entegrasyonu', () => {
  test('tüm micro frontend\'leri doğru yüklemeli', async ({ page }) => {
    await page.goto('http://localhost:3000');

    // Shell'in yüklenmesini bekle
    await expect(page.locator('[data-testid="shell-loaded"]')).toBeVisible();

    // Micro frontend'lerin yüklendiğini kontrol et
    await expect(page.locator('[data-testid="products-mf"]')).toBeVisible();
    await expect(page.locator('[data-testid="user-menu-mf"]')).toBeVisible();
  });

  test('micro frontend iletişimini yönetmeli', async ({ page }) => {
    await page.goto('http://localhost:3000/products');

    // Bir ürüne tıkla
    await page.click('[data-testid="product-card"]:first-child');

    // Sepetin güncellendiğini doğrula
    await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');

    // Bildirimin göründüğünü doğrula
    await expect(page.locator('[data-testid="notification"]')).toContainText('sepete eklendi');
  });

  test('micro frontend hatalarını zarif bir şekilde ele almalı', async ({ page }) => {
    // Bir micro frontend için ağ hatasını simüle et
    await page.route('**/products/remoteEntry.js', route => {
      route.abort();
    });

    await page.goto('http://localhost:3000');

    // Fallback UI göstermeli
    await expect(page.locator('[data-testid="products-fallback"]')).toBeVisible();

    // Diğer micro frontend'ler hala çalışmalı
    await expect(page.locator('[data-testid="user-menu-mf"]')).toBeVisible();
  });
});

Sıradaki Konular#

Paylaşılan event bus ve shell’in sahiplendiği routing ile kurulan Module Federation, varsayılan olarak alınmaya değer kurulumdur: takımlar bağımsız deploy eder ve bunun için ikinci bir runtime işletmek gerekmez. State’in neredeyse tamamı uygulamalar arasında ortaksa, tüm route’lar zaten tek takımın elindeyse veya bundle bütçesi paylaşılan bağımlılık grafiğini kaldırmıyorsa bu varsayılanı bırakın. Üç durumda da modüler monolith daha ucuza gelir.

Bölüm 3: İleri Düzey Pattern’ler, Performans ve Hata Ayıklama için devam edin:

  • Dağıtık frontend’ler arasında ileri düzey state yönetimi
  • Performans optimizasyonu ve bundle analiz teknikleri
  • Production hata ayıklama hikayeleri ve izleme stratejileri
  • Cross-origin iletişim için güvenlik pattern’leri
  • Bellek sızıntısı tespiti ve çözümü
  • Monolith’lerden migrasyon stratejileri

Seri Navigasyonu

  • Bölüm 1: Mimari temelleri
  • Bölüm 2 (Mevcut): Uygulama pattern’leri
  • Bölüm 3: İleri düzey pattern’ler & hata ayıklama

Kaynaklar#

Micro Frontend Mimari Rehberi

Micro frontend mimarisine dair 3 bölümlük kapsamlı rehber. Temel kavramlardan ileri düzey pattern'lere ve production debugging stratejilerine kadar.

İlerleme 2/3 yazı tamamlandı

Bu serideki tüm yazılar

İlgili yazılar