İçeriğe atla

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

Micro frontend kompozisyon stratejileri karşılaştırması: sunucu tarafı, build-zamanı, Module Federation ve iframe; ayrıca takasın karşılığını veren ekip ön koşulları.

Ayhan Sipahi Ayhan Sipahi

Micro frontend mimarileri, tek sayfalı bir frontend’i bağımsız olarak deploy edilebilen, bağımsız olarak sahiplenilen ve runtime’da birleşen dilimlere böler. Belirli bir dizi sorunu çözerler (ekip büyüklüğü ölçeklendirme, bağımsız sürüm temposu, teknoloji esnekliği) ve buna karşılık yeni bir dizi problem getirirler (runtime kompozisyon karmaşıklığı, paylaşılan bağımlılık yönetimi, dilimler arası state, dilimler arası performans). Benimseme kararı nadiren net bir kazançtır; koordinasyon yükünü sürüm bağımsızlığıyla değiş tokuş eder ve bu takas yalnızca belirli ekip büyüklüklerinde ve ürün yapısı sınırlarında kendini amorti eder.

Güvenli varsayılan, ekip sınırlarınızın izin verdiği en basit kompozisyondur: monorepo içinde build-zamanı entegrasyon. Runtime kompozisyonu, bağımsız sürüm temposu asıl kısıt haline gelene kadar bekler.

Micro Frontend Serisi Navigasyonu#

  • Kısım 1 (Burada bulunuyorsunuz): Mimari temelleri ve uygulama türleri
  • Kısım 2: Module Federation, iletişim kalıpları ve entegrasyon stratejileri
  • Kısım 3: İleri düzey kalıplar, performans optimizasyonu ve production hata ayıklama

Micro Frontend’ler Nedir?#

Micro frontend’ler, microservis konseptini frontend geliştirmeye genişletir. Tek bir monolitik frontend uygulaması yerine, bağımsız olarak deploy edilebilen birden çok küçük uygulamayı tek ve tutarlı bir kullanıcı deneyiminde birleştirirsiniz.

Takımlar kendi framework’lerini ve araçlarını seçer, kendi takvimlerine göre deploy eder ve ürünün bir dilimini uçtan uca sahiplenir. Bir monolith de dilim dilim taşınabilir.

Frontend’i Birleştirmenin Dört Yolu#

Server-Side Template Composition#

// 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');
  }
});

Sayfalar JavaScript olmadan çalışır ve arama motorları tamamlanmış HTML görür. Feragat ettiğiniz şey etkileşimdir: her navigasyon bir sayfa yenilemesine mal olur, paylaşılan state’in yerleşeceği doğal bir yer kalmaz. Sunucu tarafına zaten alışkın ekiplerin yürüttüğü içerik ağırlıklı siteler bundan en çok faydayı görür.

Build-Time Integration#

Micro frontend’ler npm paketleri olarak yayınlanır ve build zamanında compose edilir. Shell, her birini sürümlenmiş bir bağımlılık olarak sabitler:

{
  "dependencies": {
    "@company/header-mf": "^1.2.0",
    "@company/product-catalog-mf": "^2.1.5",
    "@company/checkout-mf": "^1.8.2"
  }
}

Shell daha sonra bunları herhangi bir paket gibi import eder:

// 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;
// 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>
  );
};

Burada deployment’lar koordineli kalır ve shell ile paketleri arasındaki sürümleri birinin yönetmesi gerekir; yani bağımsız dağıtım bu paketin içinde yoktur. Buna karşılık her dilim sıradan bir import gibi davranır: type safety, paylaşılan bağımlılık optimizasyonu ve kimsenin yeniden öğrenmesi gerekmeyen bir geliştirme deneyimi.

JavaScript ile Runtime Integration#

// Micro frontend registry
declare const __webpack_init_sharing__: (scope: string) => Promise<void>;
declare const __webpack_share_scopes__: Record<string, any>;

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);
      await __webpack_init_sharing__('default');
      const container = (window as any)[config.scope];

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

      // Remote, host'un share scope'una katılır; böylece react singleton kalır
      await container.init(__webpack_share_scopes__.default);

      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 ve hata ayıklama zorlukları.

Iframe Tabanlı Integration#

// 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 SHELL_ORIGIN = 'https://shell.example.com';

const MicroFrontendApp: React.FC = () => {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== SHELL_ORIGIN) {
        return;
      }

      if (event.data.target === 'product-catalog') {
        setData(event.data.payload);
      }
    };

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

  const sendDataToShell = (payload: any) => {
    // '*' yerine shell origin'ini açıkça belirt
    window.parent.postMessage({
      source: 'product-catalog',
      payload
    }, SHELL_ORIGIN);
  };

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

İzolasyon burada asıl amaçtır: ayrı origin, ayrı CSS kapsamı, ayrı güvenlik sınırı. Bu izolasyonun bedeli ağırdır. İletişim postMessage’a daralır, SEO zayıflar ve bir doküman içinde tam bir doküman çalıştırmak performans ve UX maliyeti getirir.

Yalnızca Production’da Kaybolan Stiller#

Bir runtime entegrasyonu geliştirme ortamında kusursuz çalışıp yine de eksik stillerle yayına çıkabilir. Micro frontend tek başına çalıştığında stiller sorunsuz görünür; production’da ise hata aralıklıdır: bazen stiller yüklenir, bazen yüklenmez.

Kök neden bir CSS yükleme race condition’ıdır.

// 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>;
};

Production’da, daha agresif minification ve CDN önbellekleme ile CSS import’u component zaten render olduktan sonra tamamlanır. Düzeltme, yükleme sırasını import zamanlamasına bırakmak yerine açıkça belirler:

// 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);
    });
  }
};

Paylaşılan Bağımlılıklar ve Yükleme Sırası#

Micro frontend’ler kendine özgü performans zorlukları getirir:

Bundle Boyutu ve Bağımlılık Tekrarı#

Birden fazla micro frontend genellikle aynı bağımlılıkları ayrı ayrı taşır, bu da şişkin bundle’lara yol açar.

// Paylaşılan bağımlılıklar için Webpack konfigürasyonu
// Module Federation 2.0, webpack'in yerleşik eklentisi değil; @module-federation/enhanced ile gelir
const { ModuleFederationPlugin } = require('@module-federation/enhanced/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
        }
      },
    }),
  ],
};

Önceliğe Göre Yükleme#

Progressive yükleme stratejileri uygulayın:

// 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} />;
};

Kısıta Göre Seçim#

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ıradaki Konular#

Kısım 2: Module Federation ve Uygulama Kalıpları şunları ele alıyor:

  • Production’a hazır Module Federation konfigürasyonları
  • Sağlam hata yönetimi ve fallback stratejileri
  • Micro frontend’ler arası iletişim kalıpları
  • Uygulamalar arası routing koordinasyonu
  • Geliştirme iş akışları ve tooling
  • Yaygın runtime hatalarında hata ayıklama adımları

Sürüm temposunun ayrışması gerektiğinde veya doğrulayamadığınız bir içeriğin sandbox içinde kalması gerektiğinde build-zamanı varsayılanından sapın. Eski bir stack’in yenisinin yanında yaşaması da runtime maliyetine değen bir durumdur.

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 1/3 yazı tamamlandı

Bu serideki tüm yazılar

İlgili yazılar