Micro Frontend Mimarisi Temelleri: Modern Web Uygulamaları İçin Eksiksiz Rehber

Micro frontend mimarisinin temelleri, uygulama stratejileri ve gerçek dünya örnekleri. Module Federation, single-spa ve diğer yaklaşımların detaylı karşılaştırması.

Frontend uygulamaları karmaşıklık ve takım boyutu olarak büyüdükçe, geçmişte bize iyi hizmet veren monolitik yaklaşım sınırlarını göstermeye başlar. Takımlar birbirlerinin ayağına basar, deployment bir koordinasyon kabusu haline gelir ve teknoloji seçimleri yıllarca kilitleniyor. Tanıdık geliyor mu?

Micro frontend'ler bu sorunlara çekici bir çözüm sunar, ancak sihirli bir değnek değiller. Birden fazla organizasyonda büyük ölçekte micro frontend mimarileri implementasyonumdan öğrendiğim şey, başarının büyük ölçüde temel kalıpları ve bunların trade-off'larını anlamaya bağlı olduğudur.

Kapsamlı Micro Frontend Serisi#

Bu, kapsamlı 3 bölümlük serinin 1. Bölümü. İşte tam öğrenme yolunuz:

Micro frontend'lere yeni misiniz? Temel kavramları anlamak için bu yazıyla başlayın, sonra seriyi sırayla takip edin.

Uygulamaya hazır mısınız? Uygulamalı Module Federation örnekleri için Kısım 2'ye geçin.

Production'da çalışıyor musunuz? İleri düzey hata ayıklama ve optimizasyon teknikleri için doğrudan Kısım 3'e gidin.

Micro Frontend'ler Nedir?#

Micro frontend'ler, microservis konseptini frontend geliştirmeye genişletir. Tek bir monolitik frontend uygulaması yerine, birden çok küçük, bağımsız olarak deploy edilebilir frontend uygulamasını uyumlu bir kullanıcı deneyimi haline getirirsiniz.

Ana prensipler şunlardır:

  • Teknoloji Agnostik: Takımlar kendi framework'leri ve araçlarını seçebilir
  • Bağımsız Deployment: Her micro frontend bağımsız olarak deploy edilebilir
  • Takım Özerkliği: Farklı takımlar uygulamanın farklı kısımlarına sahip olabilir
  • Aşamalı Migrasyon: Monolith'lerden kademeli migrasyon mümkündür

Micro Frontend Mimarisi Türleri#

Çeşitli yaklaşımları uygulama deneyimime dayalı olarak, işte dört ana mimari kalıp:

1. Server-Side Template Composition#

Farklı servislerin HTML fragment'larını render ettiği ve sunucuda compose edildiği en basit yaklaşım.

TypeScript
// Birden fazla micro frontend'i compose eden gateway servisi
import express from 'express';
import fetch from 'node-fetch';

const app = express();

app.get('/', async (req, res) => {
  try {
    // Farklı servislerden fragment'ları fetch et
    const [header, navigation, content, footer] = await Promise.all([
      fetch('http://header-service/fragment').then(r => r.text()),
      fetch('http://nav-service/fragment').then(r => r.text()),
      fetch('http://content-service/fragment').then(r => r.text()),
      fetch('http://footer-service/fragment').then(r => r.text())
    ]);

    const html = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>Compose Edilmiş Uygulama</title>
        </head>
        <body>
          ${header}
          ${navigation}
          <main>${content}</main>
          ${footer}
        </body>
      </html>
    `;

    res.send(html);
  } catch (error) {
    res.status(500).send('Error composing page');
  }
});

Artıları: Anlaması basit, iyi SEO, JavaScript olmadan çalışır Eksileri: Sınırlı etkileşim, navigasyon için sayfa yenileme, paylaşılan state zorlukları

Ne zaman kullanılır: İçerik ağırlıklı siteler, SEO kritik olduğunda, server-side geliştirmede rahat takımlar

2. Build-Time Integration#

Micro frontend'ler npm paketleri olarak yayınlanır ve build zamanında compose edilir.

TypeScript
// Shell uygulamasının Package.json'ı
{
  "dependencies": {
    "@company/header-mf": "^1.2.0",
    "@company/product-catalog-mf": "^2.1.5",
    "@company/checkout-mf": "^1.8.2"
  }
}

// Shell uygulaması
import React from 'react';
import { Header } from '@company/header-mf';
import { ProductCatalog } from '@company/product-catalog-mf';
import { Checkout } from '@company/checkout-mf';

const App: React.FC = () => {
  return (
    <div>
      <Header />
      <main>
        <ProductCatalog />
        <Checkout />
      </main>
    </div>
  );
};

export default App;
TypeScript
// Micro frontend paketi (header-mf)
import React from 'react';

export interface HeaderProps {
  user?: {
    name: string;
    avatar: string;
  };
  onLogout?: () => void;
}

export const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
  return (
    <header className="bg-blue-600 text-white p-4">
      <div className="flex justify-between items-center">
        <h1>Uygulamam</h1>
        {user && (
          <div className="flex items-center gap-2">
            <img src={user.avatar} alt={user.name} className="w-8 h-8 rounded-full" />
            <span>{user.name}</span>
            <button onClick={onLogout}>Çıkış</button>
          </div>
        )}
      </div>
    </header>
  );
};

Artıları: Type safety, paylaşılan bağımlılık optimizasyonu, tanıdık geliştirme deneyimi Eksileri: Koordineli deployment'lar, versiyon yönetimi karmaşıklığı, gerçekten bağımsız değil

Ne zaman kullanılır: Micro frontend faydaları istiyorsanız ama koordineli deployment'ları tolere edebiliyorsanız

3. JavaScript ile Runtime Integration#

Micro frontend'lerin runtime'da yüklendiği ve entegre edildiği en esnek yaklaşım.

TypeScript
// Micro frontend registry
interface MicroFrontendConfig {
  name: string;
  url: string;
  scope: string;
  module: string;
}

class MicroFrontendRegistry {
  private configs: Map<string, MicroFrontendConfig> = new Map();
  private loadedModules: Map<string, any> = new Map();

  register(config: MicroFrontendConfig) {
    this.configs.set(config.name, config);
  }

  async load(name: string): Promise<any> {
    if (this.loadedModules.has(name)) {
      return this.loadedModules.get(name);
    }

    const config = this.configs.get(name);
    if (!config) {
      throw new Error(`Micro frontend ${name} kayıtlı değil`);
    }

    // Hata yönetimi ile dinamik import
    try {
      await this.loadScript(config.url);
      const container = (window as any)[config.scope];

      if (!container) {
        throw new Error(`Container ${config.scope} bulunamadı`);
      }

      await container.init({
        react: () => Promise.resolve(React),
        'react-dom': () => Promise.resolve(ReactDOM),
      });

      const factory = await container.get(config.module);
      const Module = factory();

      this.loadedModules.set(name, Module);
      return Module;
    } catch (error) {
      console.error(`Micro frontend ${name} yüklenemedi:`, error);
      throw error;
    }
  }

  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(new Error(`Script yüklenemedi: ${url}`));
      document.head.appendChild(script);
    });
  }
}

// Shell uygulamasında kullanım
const registry = new MicroFrontendRegistry();

registry.register({
  name: 'product-catalog',
  url: 'http://localhost:3001/remoteEntry.js',
  scope: 'productCatalog',
  module: './ProductCatalog'
});

const DynamicMicroFrontend: React.FC<{ name: string }> = ({ name }) => {
  const [Component, setComponent] = useState<React.ComponentType | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    registry.load(name)
      .then(Module => {
        setComponent(() => Module.default || Module);
        setError(null);
      })
      .catch(err => {
        setError(err.message);
        setComponent(null);
      })
      .finally(() => setLoading(false));
  }, [name]);

  if (loading) return <div>{name} yükleniyor...</div>;
  if (error) return <div>{name} yükleme hatası: {error}</div>;
  if (!Component) return <div>{name} bileşeni bulunamadı</div>;

  return <Component />;
};

Artıları: Gerçek bağımsızlık, farklı teknoloji stack'leri mümkün, runtime esnekliği Eksileri: Karmaşıklık, runtime hataları, performans overhead'i, hata ayıklama zorlukları

Ne zaman kullanılır: Birden fazla takıma sahip büyük organizasyonlar, teknoloji çeşitliliği ihtiyacı

4. Iframe Tabanlı Integration#

Tam izolasyon için iframe'leri kullanan en izole yaklaşım.

TypeScript
// postMessage iletişimi ile Iframe micro frontend wrapper
interface IframeMicroFrontendProps {
  src: string;
  name: string;
  onMessage?: (data: any) => void;
}

const IframeMicroFrontend: React.FC<IframeMicroFrontendProps> = ({
  src,
  name,
  onMessage
}) => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      // Güvenlik için origin doğrula
      if (event.origin !== new URL(src).origin) {
        return;
      }

      if (event.data.source === name) {
        onMessage?.(event.data.payload);
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [src, name, onMessage]);

  const sendMessage = (data: any) => {
    if (iframeRef.current?.contentWindow) {
      iframeRef.current.contentWindow.postMessage({
        source: 'shell',
        target: name,
        payload: data
      }, new URL(src).origin);
    }
  };

  return (
    <div className="micro-frontend-container">
      {!isLoaded && <div>{name} yükleniyor...</div>}
      {error && <div>Hata: {error}</div>}
      <iframe
        ref={iframeRef}
        src={src}
        onLoad={() => setIsLoaded(true)}
        onError={() => setError(`${name} yüklenemedi`)}
        style={{
          width: '100%',
          border: 'none',
          minHeight: '400px'
        }}
        title={name}
        sandbox="allow-scripts allow-same-origin allow-forms"
      />
    </div>
  );
};

// Micro frontend içinde (iframe içeriği)
const MicroFrontendApp: React.FC = () => {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.data.target === 'product-catalog') {
        setData(event.data.payload);
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  const sendDataToShell = (payload: any) => {
    window.parent.postMessage({
      source: 'product-catalog',
      payload
    }, '*');
  };

  return (
    <div>
      <h2>Ürün Katalogu Micro Frontend</h2>
      {/* Micro frontend içeriğiniz */}
    </div>
  );
};

Artıları: Tam izolasyon, güvenlik, farklı domain'ler mümkün, CSS izolasyonu Eksileri: Sınırlı iletişim, SEO zorlukları, performans overhead'i, UX düşünceleri

Ne zaman kullanılır: Güvenlik öncelik, legacy entegrasyon, üçüncü taraf içerik

Gerçek Bir Hata Ayıklama Hikayesi: Kaybolan Stiller Vakası#

Yaygın micro frontend tuzaklarını gösteren bir hata ayıklama hikayesi paylaşayım. Yukarıdakine benzer bir runtime entegrasyon sistemi implement etmiştik ve geliştirme ortamında her şey mükemmel çalışıyordu. Ancak production'da, ürün katalogu micro frontend'imizde eksik stiller hakkında raporlar almaya başladık.

Belirtiler şaşırtıcıydı:

  • Stiller micro frontend tek başına çalıştığında gayet iyi çalışıyordu
  • Sorun sadece production'da oluyordu, geliştirme ortamında değil
  • Aralıklıydı - bazen stiller yükleniyordu, bazen yüklenmiyordu

Saatlerce araştırmadan sonra, temel nedeni keşfettik: CSS yükleme race condition'ları.

TypeScript
// Problemli kod
const ProductCatalogMF: React.FC = () => {
  useEffect(() => {
    // Bu, component mount'tan sonra CSS yüklüyordu
    import('./styles.css');
  }, []);

  return <div className="product-grid">...</div>;
};

Sorun şuydu: production'da, daha agresif minification ve CDN cache'leme ile CSS import'u component zaten render olduktan sonra tamamlanıyordu. Çözüm daha sağlam bir yükleme stratejisi gerektiriyordu:

TypeScript
// Uygun CSS yükleme ile düzeltilmiş versiyon
const MicroFrontendLoader = {
  async loadWithStyles(name: string, cssUrls: string[] = []) {
    // Önce CSS'i yükle
    await Promise.all(
      cssUrls.map(url => this.loadStylesheet(url))
    );

    // Sonra component'i yükle
    return await registry.load(name);
  },

  loadStylesheet(url: string): Promise<void> {
    return new Promise((resolve, reject) => {
      // Zaten yüklenmiş mi kontrol et
      if (document.querySelector(`link[href="${url}"]`)) {
        resolve();
        return;
      }

      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = url;
      link.onload = () => resolve();
      link.onerror = () => reject(new Error(`CSS yüklenemedi: ${url}`));
      document.head.appendChild(link);
    });
  }
};

Bu deneyim bize şunların önemini öğretti:

  1. Micro frontend mimarilerinde doğru yükleme sıralaması
  2. Environment parity - production sorunları genellikle geliştirme ortamında göstermez
  3. Monitoring ve gözlemlenebilirlik - bu sorunları erken yakalamak için CSS yükleme takibi ekledik

Performans Düşünceleri#

Micro frontend'ler benzersiz performans zorlukları sunar:

Bundle Boyutu ve Duplikasyon#

Birden fazla micro frontend genellikle aynı bağımlılıkları ship eder, bu da şişkin bundle'lara yol açar.

TypeScript
// Paylaşılan bağımlılıklar için Webpack konfigürasyonu
const ModuleFederationPlugin = require('@module-federation/webpack');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        productCatalog: 'productCatalog@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        // Ortak yardımcıları paylaş
        lodash: {
          singleton: false, // Gerekirse birden fazla versiyona izin ver
        }
      },
    }),
  ],
};

Yükleme Performansı#

Progressive yükleme stratejileri uygulayın:

TypeScript
// Progressive micro frontend yükleme
const ProgressiveMicroFrontend: React.FC<{
  name: string;
  priority: 'high' | 'medium' | 'low';
}> = ({ name, priority }) => {
  const [shouldLoad, setShouldLoad] = useState(priority === 'high');
  const isVisible = useIntersectionObserver();

  useEffect(() => {
    if (priority === 'medium' && isVisible) {
      setShouldLoad(true);
    } else if (priority === 'low') {
      // Ana içerik hazır olduktan sonra yükle
      const timer = setTimeout(() => setShouldLoad(true), 2000);
      return () => clearTimeout(timer);
    }
  }, [isVisible, priority]);

  if (!shouldLoad) {
    return <div>{name} yükleniyor...</div>;
  }

  return <DynamicMicroFrontend name={name} />;
};

Doğru Mimariyi Seçmek#

Seçim, özel kısıtlarınıza bağlıdır:

FaktörServer-SideBuild-TimeRuntimeIframe
Takım BağımsızlığıDüşükOrtaYüksekYüksek
Teknoloji ÇeşitliliğiOrtaDüşükYüksekYüksek
PerformansYüksekYüksekOrtaDüşük
KarmaşıklıkDüşükOrtaYüksekOrta
SEOMükemmelİyiZayıfZayıf
Geliştirme DeneyimiİyiMükemmelOrtaZayıf

Sırada Ne Var?#

Artık temel micro frontend kalıplarını anladığınıza göre, pratik implementasyona daha derin dalmaya hazırsınız.

Kısım 2: Module Federation ve Uygulama Kalıpları'na devam edin burada şunları kapsayacağız:

  • Production'a hazır Module Federation konfigürasyonları
  • Sağlam hata yönetimi ve fallback stratejileri
  • Cross-micro frontend iletişim kalıpları
  • Uygulamalar arası routing koordinasyonu
  • Geliştirme iş akışları ve tooling
  • Production sistemlerinden gerçek hata ayıklama hikayeleri

Anahtar Öğreti: Micro frontend'ler sadece teknik bir kalıp değil - takım yapınız, iş gereksinimleri ve teknik kısıtlarınızın dikkatli değerlendirmesini gerektiren organizasyonel bir kalıptır.

Burada kapsanan temel kalıplar mimari kararlarınızı yönlendirecek, ancak gerçek karmaşıklık entegrasyon katmanında ortaya çıkar - bunu bir sonraki yazıda ele alacağız.


Seri Navigasyonu

  • Kısım 1 (Mevcut): Mimari temelleri
  • Kısım 2: Uygulama kalıpları
  • Kısım 3: İleri düzey kalıplar ve hata ayıklama
Loading...

Yorumlar (0)

Sohbete katıl

Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap

Henüz yorum yok

Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!

Related Posts