Micro Frontend'ler: İleri Düzey Pattern'ler, Performans ve Production Dersleri
Micro frontend'lerde ileri düzey kalıplar: event sourcing ile paylaşımlı state, bundle bütçeleri, cross-app hata takibi, imzalı mesajlaşma ve strangler-fig geçişi.
Micro Frontend Serisi Navigasyonu#
- Kısım 1: Mimari temelleri ve uygulama türleri
- Kısım 2: Module Federation, iletişim kalıpları ve entegrasyon stratejileri
- Kısım 3 (Burada bulunuyorsunuz): İleri düzey kalıplar, performans optimizasyonu ve production hata ayıklama
Ön Koşullar: Kısım 1 temelleri ve Kısım 2 uygulama kalıpları.
Production’daki micro frontend’ler, mimari diyagramların nadiren gösterdiği zorlu problemler ortaya çıkarır: bağımsız olarak deploy edilen uygulamalar arasında ayrışan paylaşımlı state, yükleme sürelerini şişiren bundle tekrarları ve takım ile deployment sınırlarını aşan hatalar. Bu yükün büyük kısmını üç kalıp taşır. Event sourcing, bağımsız olarak deploy edilen uygulamalar arasında paylaşımlı state’i tutarlı tutar; performans bütçelemesi bundle tekrarlarını sınırlar; uygulama genelinde hata takibi ise dağıtık frontend hatalarını ayıklanabilir kılar.
İleri Düzey State Management Kalıpları#
Micro frontend mimarisinin en zor kısmı, bağımsız olarak deploy edilen uygulamalar arasında paylaşılan state’tir. Aşağıdaki iki kalıp, deploy’ları birbirine bağlamadan bu state’i tutarlı tutar.
1. Event Sourcing ile Dağıtık State Management#
// @company/shared-state - Micro frontend'ler için ileri düzey state management
interface StateEvent {
id: string;
type: string;
payload: any;
timestamp: number;
version: number;
microfrontend: string;
}
interface StateSnapshot {
version: number;
data: any;
timestamp: number;
}
class DistributedStateManager {
private eventStore: StateEvent[] = [];
private snapshots: Map<string, StateSnapshot> = new Map();
private subscribers: Map<string, Set<(state: any) => void>> = new Map();
private maxEventHistory = 1000;
private snapshotInterval = 100; // Her 100 event'te snapshot oluştur
constructor(private persistenceAdapter?: PersistenceAdapter) {
// Persistence katmanından başlangıç state'ini yükle
this.loadInitialState();
}
// Sadece ekleme yapılan event store
dispatch(event: Omit<StateEvent, 'id' | 'timestamp' | 'version'>) {
const stateEvent: StateEvent = {
...event,
id: generateId(),
timestamp: Date.now(),
version: this.eventStore.length + 1,
};
this.eventStore.push(stateEvent);
// Performans için periyodik snapshot'lar oluştur
if (stateEvent.version % this.snapshotInterval === 0) {
this.createSnapshot(event.type.split(':')[0]); // event type'dan namespace
}
// Harici storage'a kaydet
this.persistenceAdapter?.persistEvent(stateEvent);
// Subscriber'ları bilgilendir
this.notifySubscribers(event.type, this.getState(event.type.split(':')[0]));
// Event geçmişi boyutunu koru
if (this.eventStore.length > this.maxEventHistory) {
this.eventStore = this.eventStore.slice(-this.maxEventHistory);
}
}
getState(namespace: string): any {
// Önce en son snapshot'ı kullanmaya çalış
const snapshot = this.snapshots.get(namespace);
const eventsAfterSnapshot = snapshot
? this.eventStore.filter(e => e.version > snapshot.version && e.type.startsWith(namespace))
: this.eventStore.filter(e => e.type.startsWith(namespace));
let state = snapshot?.data || {};
// Snapshot'tan sonraki event'leri uygula
eventsAfterSnapshot.forEach(event => {
state = this.applyEvent(state, event);
});
return state;
}
subscribe(eventType: string, callback: (state: any) => void): () => void {
if (!this.subscribers.has(eventType)) {
this.subscribers.set(eventType, new Set());
}
this.subscribers.get(eventType)!.add(callback);
// Mevcut state ile hemen çağır
const namespace = eventType.split(':')[0];
callback(this.getState(namespace));
// Unsubscribe fonksiyonu döndür
return () => {
this.subscribers.get(eventType)?.delete(callback);
};
}
private applyEvent(state: any, event: StateEvent): any {
switch (event.type) {
case 'user:login':
return { ...state, user: event.payload, isAuthenticated: true };
case 'user:logout':
return { ...state, user: null, isAuthenticated: false };
case 'cart:add-item':
const items = state.items || [];
return {
...state,
items: [...items, event.payload],
total: calculateTotal([...items, event.payload])
};
case 'cart:remove-item':
const filteredItems = (state.items || []).filter(
(item: any) => item.id !== event.payload.id
);
return {
...state,
items: filteredItems,
total: calculateTotal(filteredItems)
};
default:
return state;
}
}
private createSnapshot(namespace: string) {
const state = this.getState(namespace);
const snapshot: StateSnapshot = {
version: this.eventStore.length,
data: state,
timestamp: Date.now(),
};
this.snapshots.set(namespace, snapshot);
this.persistenceAdapter?.persistSnapshot(namespace, snapshot);
}
private notifySubscribers(eventType: string, state: any) {
// Tam eşleşen aboneler ve "cart:*" gibi namespace joker abonelikleri
const namespace = eventType.split(':')[0];
const callbacks = new Set([
...(this.subscribers.get(eventType) ?? []),
...(this.subscribers.get(`${namespace}:*`) ?? []),
]);
callbacks.forEach(callback => {
try {
callback(state);
} catch (error) {
console.error(`Error in state subscriber for ${eventType}:`, error);
}
});
}
private async loadInitialState() {
if (this.persistenceAdapter) {
try {
const { events, snapshots } = await this.persistenceAdapter.loadInitialState();
this.eventStore = events;
snapshots.forEach((snapshot, namespace) => {
this.snapshots.set(namespace, snapshot);
});
} catch (error) {
console.error('Failed to load initial state:', error);
}
}
}
// Debug yardımcıları
getEventHistory(namespace?: string): StateEvent[] {
if (namespace) {
return this.eventStore.filter(e => e.type.startsWith(namespace));
}
return [...this.eventStore];
}
replayEventsFrom(version: number): void {
const eventsToReplay = this.eventStore.filter(e => e.version >= version);
console.log(`${version} versiyonundan ${eventsToReplay.length} event'i yeniden oynatılıyor`);
eventsToReplay.forEach(event => {
console.log(`Yeniden oynatılıyor: ${event.type}`, event.payload);
});
}
}
// Persistence adapter arayüzü
interface PersistenceAdapter {
persistEvent(event: StateEvent): Promise<void>;
persistSnapshot(namespace: string, snapshot: StateSnapshot): Promise<void>;
loadInitialState(): Promise<{
events: StateEvent[];
snapshots: Map<string, StateSnapshot>;
}>;
}
// Daha kolay kullanım için React hook'ları
export const useDistributedState = <T>(namespace: string): [T, (event: any) => void] => {
const [state, setState] = useState<T>({} as T);
const stateManager = useContext(StateManagerContext);
useEffect(() => {
const unsubscribe = stateManager.subscribe(`${namespace}:*`, setState);
return unsubscribe;
}, [namespace, stateManager]);
const dispatch = useCallback((event: any) => {
stateManager.dispatch({
type: `${namespace}:${event.type}`,
payload: event.payload,
microfrontend: event.source || 'unknown',
});
}, [namespace, stateManager]);
return [state, dispatch];
};
2. Çakışma Çözümü ile Optimistic Update’ler#
// Micro frontend'ler için gelişmiş optimistic update kalıbı
interface OptimisticUpdate {
id: string;
type: string;
payload: any;
timestamp: number;
microfrontend: string;
status: 'pending' | 'confirmed' | 'failed';
}
class OptimisticStateManager {
private pendingUpdates: Map<string, OptimisticUpdate> = new Map();
private baseState: any = {};
private stateManager: DistributedStateManager;
constructor(stateManager: DistributedStateManager) {
this.stateManager = stateManager;
}
optimisticDispatch(event: any): string {
const updateId = generateId();
const optimisticUpdate: OptimisticUpdate = {
id: updateId,
type: event.type,
payload: event.payload,
timestamp: Date.now(),
microfrontend: event.source,
status: 'pending',
};
this.pendingUpdates.set(updateId, optimisticUpdate);
// Optimistic update'i hemen uygula
this.applyOptimisticUpdate(optimisticUpdate);
// Sunucuya gönder
this.sendToServer(event)
.then(() => {
// Update'i onayla
const update = this.pendingUpdates.get(updateId);
if (update) {
update.status = 'confirmed';
// Onaylanmış state'i uygula
this.stateManager.dispatch(event);
}
})
.catch((error) => {
// Optimistic update'i geri al
const update = this.pendingUpdates.get(updateId);
if (update) {
update.status = 'failed';
this.revertOptimisticUpdate(updateId);
// Kullanıcıya hata göster
this.showConflictResolution(error, event);
}
})
.finally(() => {
this.pendingUpdates.delete(updateId);
});
return updateId;
}
private applyOptimisticUpdate(update: OptimisticUpdate) {
// Update'i UI'ya optimistic olarak uygula
const event = {
type: update.type,
payload: { ...update.payload, _optimistic: true },
microfrontend: update.microfrontend,
};
this.stateManager.dispatch(event);
}
private revertOptimisticUpdate(updateId: string) {
const update = this.pendingUpdates.get(updateId);
if (!update) return;
// Geri alma event'ini gönder
this.stateManager.dispatch({
type: `${update.type}:revert`,
payload: { originalPayload: update.payload },
microfrontend: update.microfrontend,
});
}
private async sendToServer(event: any): Promise<any> {
const response = await fetch('/api/state/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
if (!response.ok) {
throw new Error(`Sunucu update'i reddetti: ${response.statusText}`);
}
return response.json();
}
private showConflictResolution(error: Error, originalEvent: any) {
// Çakışma çözümü için UI göster
this.stateManager.dispatch({
type: 'ui:show-conflict-resolution',
payload: {
error: error.message,
originalEvent,
timestamp: Date.now(),
},
microfrontend: 'system',
});
}
}
Performans Optimizasyon Stratejileri#
1. İleri Düzey Bundle Analizi ve Optimizasyon#
// webpack-bundle-analyzer-reporter.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
class MicroFrontendBundleReporter {
constructor(options = {}) {
this.options = {
reportDir: './bundle-reports',
threshold: 50000, // 50KB
...options,
};
}
apply(compiler) {
compiler.hooks.emit.tapAsync('MicroFrontendBundleReporter', (compilation, callback) => {
const stats = compilation.getStats().toJson();
const analysis = this.analyzeBundle(stats);
// Rapor oluştur
this.generateReport(analysis);
// Büyük bundle'lar hakkında uyarı ver
this.checkBundleSize(analysis);
callback();
});
}
analyzeBundle(stats) {
const analysis = {
totalSize: 0,
sharedDependencies: {},
duplicatedDependencies: [],
largeDependencies: [],
unusedExports: [],
};
stats.chunks.forEach(chunk => {
chunk.modules.forEach(module => {
const size = module.size || 0;
analysis.totalSize += size;
// Paylaşılan bağımlılıkları tanımla
if (module.name && module.name.includes('node_modules')) {
const dep = this.extractDependencyName(module.name);
if (!analysis.sharedDependencies[dep]) {
analysis.sharedDependencies[dep] = { size: 0, count: 0 };
}
analysis.sharedDependencies[dep].size += size;
analysis.sharedDependencies[dep].count++;
}
// Büyük bağımlılıkları işaretle
if (size > this.options.threshold) {
analysis.largeDependencies.push({
name: module.name,
size,
reasons: module.reasons || [],
});
}
});
});
// Duplike bağımlılıkları bul
Object.entries(analysis.sharedDependencies).forEach(([dep, info]) => {
if (info.count > 1) {
analysis.duplicatedDependencies.push({ name: dep, ...info });
}
});
return analysis;
}
generateReport(analysis) {
const report = {
timestamp: new Date().toISOString(),
microfrontend: process.env.MICRO_FRONTEND_NAME || 'unknown',
...analysis,
};
const fs = require('fs');
const path = require('path');
if (!fs.existsSync(this.options.reportDir)) {
fs.mkdirSync(this.options.reportDir, { recursive: true });
}
const reportPath = path.join(
this.options.reportDir,
`bundle-report-${Date.now()}.json`
);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`Bundle raporu oluşturuldu: ${reportPath}`);
}
checkBundleSize(analysis) {
const maxSize = 500000; // 500KB uyarı eşiği
if (analysis.totalSize > maxSize) {
console.warn(`[WARN] Bundle boyutu (${analysis.totalSize} byte) önerilen eşiği (${maxSize} byte) aşıyor`);
}
if (analysis.duplicatedDependencies.length > 0) {
console.warn('[WARN] Duplike bağımlılıklar bulundu:');
analysis.duplicatedDependencies.forEach(dep => {
console.warn(` - ${dep.name}: ${dep.size} byte (${dep.count} kopya)`);
});
}
}
extractDependencyName(moduleName) {
const match = moduleName.match(/node_modules[\/\\](@[^\/\\]+[\/\\][^\/\\]+|[^\/\\]+)/);
return match ? match[1] : moduleName;
}
}
module.exports = MicroFrontendBundleReporter;
2. Performans İzleme ve Optimizasyon#
// @company/performance-monitor
interface PerformanceMetrics {
microfrontend: string;
loadTime: number;
renderTime: number;
bundleSize: number;
memoryUsage: number;
errorCount: number;
}
class MicroFrontendPerformanceMonitor {
private metrics: Map<string, PerformanceMetrics> = new Map();
private observers: PerformanceObserver[] = [];
constructor(private reportingEndpoint?: string) {
this.setupPerformanceObservers();
}
startMeasurement(microfrontendName: string) {
const startTime = performance.now();
return {
recordLoadComplete: () => {
const loadTime = performance.now() - startTime;
this.updateMetrics(microfrontendName, { loadTime });
},
recordRenderComplete: () => {
const renderTime = performance.now() - startTime;
this.updateMetrics(microfrontendName, { renderTime });
},
recordError: () => {
const current = this.metrics.get(microfrontendName);
this.updateMetrics(microfrontendName, {
errorCount: (current?.errorCount || 0) + 1
});
}
};
}
private setupPerformanceObservers() {
// Resource yüklemeyi izle
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes('remoteEntry.js')) {
const microfrontendName = this.extractMicrofrontendName(entry.name);
this.updateMetrics(microfrontendName, {
bundleSize: entry.transferSize || 0,
});
}
});
});
resourceObserver.observe({ entryTypes: ['resource'] });
this.observers.push(resourceObserver);
// Uzun task'ları izle
const longTaskObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 16) { // 16ms'den uzun task'lar (60fps'de 1 frame)
console.warn(`Uzun task algılandı: ${entry.duration}ms`);
this.reportLongTask(entry);
}
});
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
this.observers.push(longTaskObserver);
// Bellek kullanımını izle
this.startMemoryMonitoring();
}
private startMemoryMonitoring() {
setInterval(() => {
if ('memory' in performance) {
const memInfo = (performance as any).memory;
this.metrics.forEach((_, microfrontendName) => {
this.updateMetrics(microfrontendName, {
memoryUsage: memInfo.usedJSHeapSize,
});
});
}
}, 10000); // Her 10 saniyede
}
private updateMetrics(microfrontendName: string, updates: Partial<PerformanceMetrics>) {
const current = this.metrics.get(microfrontendName) || {
microfrontend: microfrontendName,
loadTime: 0,
renderTime: 0,
bundleSize: 0,
memoryUsage: 0,
errorCount: 0,
};
const updated = { ...current, ...updates };
this.metrics.set(microfrontendName, updated);
// İzleme servisine rapor et
this.reportMetrics(updated);
}
private reportMetrics(metrics: PerformanceMetrics) {
if (this.reportingEndpoint) {
fetch(this.reportingEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metrics),
}).catch(error => {
console.error('Metrik raporlama başarısız:', error);
});
}
// Geliştirme ortamında console'a da logla
if (process.env.NODE_ENV === 'development') {
console.log(`[Performance] ${metrics.microfrontend}:`, metrics);
}
}
private reportLongTask(entry: PerformanceEntry) {
if (this.reportingEndpoint) {
fetch(`${this.reportingEndpoint}/long-tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
duration: entry.duration,
startTime: entry.startTime,
timestamp: Date.now(),
}),
}).catch(error => {
console.error('Uzun task raporlama başarısız:', error);
});
}
}
getMetrics(microfrontendName?: string): PerformanceMetrics | Map<string, PerformanceMetrics> {
if (microfrontendName) {
return this.metrics.get(microfrontendName)!;
}
return new Map(this.metrics);
}
cleanup() {
this.observers.forEach(observer => observer.disconnect());
}
private extractMicrofrontendName(url: string): string {
const match = url.match(/\/\/([^.]+)\./);
return match ? match[1] : 'unknown';
}
}
// Performans izleme için React hook
export const usePerformanceMonitor = (microfrontendName: string) => {
const monitor = useContext(PerformanceMonitorContext);
useEffect(() => {
const measurement = monitor.startMeasurement(microfrontendName);
// Component mount olduğunda kaydet (render tamamlandı)
measurement.recordRenderComplete();
return () => {
// Gerekirse temizlik
};
}, [microfrontendName, monitor]);
const recordError = useCallback(() => {
const measurement = monitor.startMeasurement(microfrontendName);
measurement.recordError();
}, [microfrontendName, monitor]);
return { recordError };
};
Dağıtık Frontend’lerde Hata Ayıklama Kalıpları#
Event Bus Aboneliklerinden Doğan Bellek Sızıntıları#
Yalnızca birkaç saatlik kesintisiz kullanımdan sonra görünen sızıntı, paylaşımlı event bus’ın klasik arıza biçimidir. Uygulama yavaş yavaş ağırlaşır, sonunda bazı micro frontend’ler yanıt vermeyi bırakır. Nedeni genellikle, kendisini kaydeden component’ten daha uzun yaşayan ve tüm component scope’unu ayakta tutan bir aboneliktir:
// Bellek sızıntısına neden olan problemli kod
const ProductList: React.FC = () => {
const eventBus = useContext(EventBusContext);
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
// Bu, tüm component'i referans olarak tutan bir closure oluşturuyordu
const handler = (event: any) => {
// Handler closure tüm component scope'unu yakalar
setProducts(prevProducts => {
// Eski state'i tutan karmaşık state güncellemeleri
return updateProductsWithComplexLogic(prevProducts, event);
});
};
eventBus.subscribe('cart:updated', handler);
// Bazı kod yollarında erken return'ler nedeniyle unsubscribe hiç çağrılmıyordu
return () => eventBus.unsubscribe('cart:updated', handler);
}, []); // Eksik bağımlılıklar stale closure'lara neden oldu
return <div className="product-list">{/* ... */}</div>;
};
Çözüm, sabit bir handler referansı ve her koşulda çalışan bir temizlik yolu:
// Uygun bellek yönetimi ile düzeltilmiş versiyon
const ProductList: React.FC = () => {
const eventBus = useContext(EventBusContext);
const [products, setProducts] = useState<Product[]>([]);
// Gereksiz yeniden oluşturmaları önlemek için useCallback kullan
const handleCartUpdate = useCallback((event: CartUpdateEvent) => {
setProducts(prevProducts => {
// Referans döngülerini önlemek için immutable güncellemeler kullan
return prevProducts.map(product =>
product.id === event.productId
? { ...product, inCart: event.inCart }
: product
);
});
}, []); // Fonksiyonel güncellemeler kullandığımız için bağımlılık gerekmez
useEffect(() => {
const unsubscribe = eventBus.subscribe('cart:updated', handleCartUpdate);
// Her zaman temizliğin gerçekleşmesini sağla
return () => {
unsubscribe();
};
}, [eventBus, handleCartUpdate]);
// Geliştirme ortamında bellek kullanımını izle
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
const interval = setInterval(() => {
if ('memory' in performance) {
const memInfo = (performance as any).memory;
console.log(`ProductList bellek kullanımı: ${memInfo.usedJSHeapSize / 1024 / 1024} MB`);
}
}, 5000);
return () => clearInterval(interval);
}
}, []);
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
// Bellek sızıntısı tespit yardımcısı
class MemoryLeakDetector {
private snapshots: any[] = [];
private interval: NodeJS.Timeout;
constructor(private intervalMs: number = 30000) {
this.interval = setInterval(() => {
this.takeSnapshot();
}, intervalMs);
}
private takeSnapshot() {
if ('memory' in performance) {
const memInfo = (performance as any).memory;
const snapshot = {
timestamp: Date.now(),
usedJSHeapSize: memInfo.usedJSHeapSize,
totalJSHeapSize: memInfo.totalJSHeapSize,
jsHeapSizeLimit: memInfo.jsHeapSizeLimit,
};
this.snapshots.push(snapshot);
// Son 20 snapshot'ı tut
if (this.snapshots.length > 20) {
this.snapshots.shift();
}
this.analyzeMemoryTrend();
}
}
private analyzeMemoryTrend() {
if (this.snapshots.length < 5) return;
const recent = this.snapshots.slice(-5);
// Bellek büyüme hızını ve desenini hesapla
let consecutiveIncreases = 0;
let totalGrowth = 0;
for (let i = 1; i < recent.length; i++) {
const growth = recent[i].usedJSHeapSize - recent[i - 1].usedJSHeapSize;
if (growth > 0) {
consecutiveIncreases++;
totalGrowth += growth;
} else {
consecutiveIncreases = 0; // Düşüşte sayacı sıfırla
}
}
// Şüpheli desenleri tespit et
const growthMB = totalGrowth / 1024 / 1024;
const hasConsistentGrowth = consecutiveIncreases >= 3;
const hasSignificantGrowth = growthMB > 5; // Pencere boyunca 5 MB
if (hasConsistentGrowth && hasSignificantGrowth) {
console.warn(`Potansiyel bellek sızıntısı algılandı. Büyüme: ${recent.length - 1} aralıkta ${growthMB.toFixed(2)} MB`);
this.reportMemoryLeak(recent);
}
}
private reportMemoryLeak(snapshots: any[]) {
// İzleme servisine rapor et
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Memory Leak Detected', {
snapshots,
userAgent: navigator.userAgent,
timestamp: Date.now(),
});
}
}
cleanup() {
if (this.interval) {
clearInterval(this.interval);
}
}
}
Aralıklı Cross-Origin Mesaj Kayıpları#
Farklı subdomain’lerde deploy edilen micro frontend’ler mesajları aralıklı olarak kaybeder. postMessage, hedef frame yüklüyse mesajı iletir; yüklü değilse sessizce hiçbir yere ulaşmaz. Ortada bir hata fırlatılmadığı için arıza rastgele görünür. Sınırlı sayıda yeniden deneme yapan bir kuyruk zamanlama bağımlılığını ortadan kaldırır; sıkı origin kontrolü de kanalın bir giriş noktasına dönüşmesini engeller:
// Çözüm: Sağlam cross-origin iletişim
class CrossOriginMessenger {
private trustedOrigins: Set<string> = new Set();
private messageQueue: Array<{ data: any; targetOrigin: string; retry: number }> = [];
private maxRetries = 3;
constructor(trustedOrigins: string[]) {
trustedOrigins.forEach(origin => this.trustedOrigins.add(origin));
this.setupMessageListener();
this.startRetryProcessor();
}
private setupMessageListener() {
window.addEventListener('message', (event) => {
// Sıkı origin kontrolü
if (!this.trustedOrigins.has(event.origin)) {
console.warn(`Güvenilmeyen origin'den mesaj reddedildi: ${event.origin}`);
return;
}
try {
const message = JSON.parse(event.data);
this.handleMessage(message, event.origin);
} catch (error) {
console.error('Cross-origin mesajı ayrıştırılamadı:', error);
}
});
}
sendMessage(data: any, targetOrigin: string, retries: number = 0): boolean {
if (!this.trustedOrigins.has(targetOrigin)) {
console.error(`Güvenilmeyen origin'e mesaj gönderme denemesi: ${targetOrigin}`);
return false;
}
try {
const serializedData = JSON.stringify({
...data,
timestamp: Date.now(),
sender: window.location.origin,
messageId: generateId(),
});
// Hedef window'u bulmaya çalış
const targetWindow = this.findTargetWindow(targetOrigin);
if (targetWindow) {
targetWindow.postMessage(serializedData, targetOrigin);
return true;
} else {
// Daha sonra tekrar denemek için kuyruğa al
this.messageQueue.push({ data, targetOrigin, retry: retries });
return false;
}
} catch (error) {
console.error('Cross-origin mesaj gönderme başarısız:', error);
return false;
}
}
private findTargetWindow(targetOrigin: string): Window | null {
// Tüm iframe'leri kontrol et
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
try {
if (iframe.src.startsWith(targetOrigin)) {
return iframe.contentWindow;
}
} catch (error) {
// Erişim reddedildi - muhtemelen cross-origin
continue;
}
}
// Parent window olup olmadığını kontrol et
if (window.parent !== window) {
try {
if (document.referrer.startsWith(targetOrigin)) {
return window.parent;
}
} catch (error) {
// Erişim reddedildi
}
}
return null;
}
private startRetryProcessor() {
setInterval(() => {
const toRetry = this.messageQueue.splice(0); // Tüm kuyruktaki mesajları al
toRetry.forEach(({ data, targetOrigin, retry }) => {
if (retry < this.maxRetries) {
const success = this.sendMessage(data, targetOrigin, retry + 1);
if (!success) {
// Artan retry sayısı ile yeniden kuyruğa al
this.messageQueue.push({ data, targetOrigin, retry: retry + 1 });
}
} else {
console.error(`${this.maxRetries} denemeden sonra mesaj gönderilemedi:`, data);
}
});
}, 1000); // Her saniye tekrar dene
}
private handleMessage(message: any, origin: string) {
// Farklı mesaj türlerini ele al
switch (message.type) {
case 'state-update':
this.handleStateUpdate(message.payload);
break;
case 'navigation':
this.handleNavigation(message.payload);
break;
case 'error':
this.handleError(message.payload);
break;
default:
console.warn(`Bilinmeyen mesaj türü: ${message.type}`);
}
}
private handleStateUpdate(payload: any) {
// Paylaşılan state'i güncelle
if (typeof window !== 'undefined' && (window as any).stateManager) {
(window as any).stateManager.dispatch({
type: payload.type,
payload: payload.data,
source: 'cross-origin',
});
}
}
private handleNavigation(payload: any) {
// Navigasyon isteklerini ele al
if (payload.path && typeof window !== 'undefined') {
window.history.pushState({}, '', payload.path);
}
}
private handleError(payload: any) {
console.error('Cross-origin hata alındı:', payload);
// İzleme servisine rapor et
}
}
Micro Frontend Güvenliği#
1. Dinamik Yükleme için Content Security Policy (CSP)#
// Micro frontend uygulamaları için CSP header üretici
class MicroFrontendCSPGenerator {
constructor(
private allowedOrigins: string[],
private isDevelopment: boolean = false
) {}
generateCSP(): string {
const directives = [];
// Script kaynakları - kendi origin ve micro frontend origin'lerini izin ver
const scriptSrc = [
"'self'",
...this.allowedOrigins,
];
if (this.isDevelopment) {
scriptSrc.push("'unsafe-eval'"); // Geliştirme araçları için
}
directives.push(`script-src ${scriptSrc.join(' ')}`);
// API çağrıları için connect kaynakları
const connectSrc = [
"'self'",
...this.allowedOrigins,
// API endpoint'leri ekle
'https://api.company.com',
];
directives.push(`connect-src ${connectSrc.join(' ')}`);
// iframe tabanlı micro frontend'ler için frame kaynakları
const frameSrc = [
"'self'",
...this.allowedOrigins,
];
directives.push(`frame-src ${frameSrc.join(' ')}`);
// Image kaynakları
directives.push(`img-src 'self' data: https:`);
// Style kaynakları - unsafe-inline yerine nonce tabanlı yaklaşım
const styleSrc = [
"'self'",
"'nonce-{NONCE_VALUE}'", // Production'da gerçek nonce ile değiştir
...this.allowedOrigins,
];
// Sadece development'ta hot reloading için unsafe-inline'a izin ver
if (this.isDevelopment) {
styleSrc.push("'unsafe-inline'");
}
directives.push(`style-src ${styleSrc.join(' ')}`);
return directives.join('; ');
}
// Express.js için middleware
middleware() {
return (req: any, res: any, next: any) => {
res.setHeader('Content-Security-Policy', this.generateCSP());
next();
};
}
}
// Kullanım
const cspGenerator = new MicroFrontendCSPGenerator([
'https://products.company.com',
'https://cart.company.com',
'https://user.company.com',
], process.env.NODE_ENV === 'development');
app.use(cspGenerator.middleware());
2. Micro Frontend’ler Arası Güvenli İletişim#
// Güvenli iletişim kanalı
class SecureMicroFrontendCommunication {
private secretKey: string;
private trustedOrigins: Set<string>;
constructor(secretKey: string, trustedOrigins: string[]) {
this.secretKey = secretKey;
this.trustedOrigins = new Set(trustedOrigins);
}
async sendSecureMessage(data: any, targetOrigin: string): Promise<boolean> {
if (!this.trustedOrigins.has(targetOrigin)) {
throw new Error(`Güvenilmeyen origin: ${targetOrigin}`);
}
try {
// Timestamp ve nonce ile mesaj oluştur
const message = {
data,
timestamp: Date.now(),
nonce: this.generateNonce(),
};
// Mesajı imzala
const signature = await this.signMessage(message);
const secureMessage = { ...message, signature };
// postMessage ile gönder
const targetWindow = this.findTargetWindow(targetOrigin);
if (targetWindow) {
targetWindow.postMessage(JSON.stringify(secureMessage), targetOrigin);
return true;
}
return false;
} catch (error) {
console.error('Güvenli mesaj gönderme başarısız:', error);
return false;
}
}
async verifyAndHandleMessage(event: MessageEvent): Promise<boolean> {
if (!this.trustedOrigins.has(event.origin)) {
console.warn(`Güvenilmeyen origin'den mesaj: ${event.origin}`);
return false;
}
try {
const message = JSON.parse(event.data);
// Timestamp doğrula (replay saldırılarını önle)
const age = Date.now() - message.timestamp;
if (age > 60000) { // Maksimum 1 dakika yaş
console.warn('Mesaj çok eski, potansiyel replay saldırısı');
return false;
}
// İmzayı doğrula
const isValid = await this.verifySignature(message);
if (!isValid) {
console.warn('Geçersiz mesaj imzası');
return false;
}
// Mesajı işle
this.handleVerifiedMessage(message.data);
return true;
} catch (error) {
console.error('Mesaj doğrulama başarısız:', error);
return false;
}
}
private async signMessage(message: any): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const keyData = encoder.encode(this.secretKey);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, data);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
private async verifySignature(message: any): Promise<boolean> {
const { signature, ...messageWithoutSignature } = message;
const expectedSignature = await this.signMessage(messageWithoutSignature);
return signature === expectedSignature;
}
private generateNonce(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
private findTargetWindow(targetOrigin: string): Window | null {
// Önceki örnekteki implementasyona benzer
return null; // Kısalık için basitleştirildi
}
private handleVerifiedMessage(data: any) {
// Doğrulanmış mesajı ele al
console.log('Doğrulanmış mesaj alındı:', data);
}
}
Migrasyon Stratejileri#
Strangler Fig Pattern Uygulaması#
// Monolith'ten micro frontend'lere kademeli migrasyon
class StranglerFigMigration {
private routes: Map<string, 'monolith' | 'microfrontend'> = new Map();
private featureFlags: Map<string, boolean> = new Map();
constructor(private config: MigrationConfig) {
this.initializeRoutes();
}
private initializeRoutes() {
// Tüm route'lar monolith'e gidecek şekilde başla
this.config.allRoutes.forEach(route => {
this.routes.set(route, 'monolith');
});
// Feature flag'lara göre kademeli olarak micro frontend route'larını etkinleştir
this.config.microfrontendRoutes.forEach(route => {
const flagName = `enable_mf_${route.replace(/[^a-zA-Z0-9]/g, '_')}`;
if (this.featureFlags.get(flagName)) {
this.routes.set(route, 'microfrontend');
}
});
}
routeRequest(path: string): 'monolith' | 'microfrontend' {
// Önce tam eşleşme kontrolü
if (this.routes.has(path)) {
return this.routes.get(path)!;
}
// Pattern eşleşmeleri kontrol et
for (const [route, target] of this.routes.entries()) {
if (this.matchesPattern(path, route)) {
return target;
}
}
// Bilinmeyen route'lar için monolith varsayılan
return 'monolith';
}
migrateRoute(route: string) {
console.log(`${route} route'u micro frontend'e migrate ediliyor`);
this.routes.set(route, 'microfrontend');
// İzleme için migrasyon event'ini logla
this.logMigrationEvent(route);
}
rollbackRoute(route: string) {
console.log(`${route} route'u monolith'e geri alınıyor`);
this.routes.set(route, 'monolith');
// Rollback event'ini logla
this.logRollbackEvent(route);
}
private matchesPattern(path: string, pattern: string): boolean {
// Basit wildcard eşleştirme
const regex = new RegExp(
'^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
);
return regex.test(path);
}
private logMigrationEvent(route: string) {
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Route Migrated', {
route,
timestamp: Date.now(),
});
}
}
private logRollbackEvent(route: string) {
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Route Rolled Back', {
route,
timestamp: Date.now(),
});
}
}
}
interface MigrationConfig {
allRoutes: string[];
microfrontendRoutes: string[];
}
Ekosistem Görünümü#
Micro frontend ekosistemi hareket etmeye devam ediyor. Takip etmeye değer üç yön var:
- Module Federation’ın yanında single-spa gibi framework-agnostic orkestrasyon
- CDN sağlayıcılarının parçaları yanıt tarayıcıya ulaşmadan birleştirdiği edge-side composition
- Her parça hazır olmadan kabuğu göndermeye başlayan streaming mimariler
Bu Kalıplar Nerede Geçerli#
Event sourcing, performans bütçelemesi ve uygulama genelinde hata takibi, birkaç takım paylaşımlı state üzerinde bağımsız deploy almaya başladığında karşılığını verir. Bu ölçeğin altında tutulan defter, ortadan kaldırdığı koordinasyondan pahalıya gelir: tek bundle gönderen tek takım için build-time integration ve sade bir store daha uygundur. Runtime yüklemeyi ancak bağımsız deploy’lar darboğaza dönüştüğünde ekleyin. İki maliyet her ölçekte sabit kalır. Her dinamik yükleme başarısız olabilir, bu yüzden nazik degradation ilk sürüme aittir ve sonraki bir sağlamlaştırma turuna bırakılamaz; her cross-origin mesaj sınırı da açıldığı günden itibaren origin kontrolü ve imza ister.
Geçişe yeni başlıyorsanız, monolith’teki tek bir route’u feature flag arkasında strangler-fig yönlendiricisine taşıyın; böylece geri dönüş bir konfigürasyon değişikliği olarak kalır.
Tam Seri Navigasyonu
- Kısım 1: Mimari temelleri
- Kısım 2: Uygulama kalıpları
- Kısım 3 (Mevcut): İleri düzey kalıplar ve hata ayıklama
Kaynaklar#
- Micro Frontends - Martin Fowler (yeni sekmede açılır) - Micro frontend mimarisinin kalıplarını ve trade-off’larını tanımlayan, Cam Jackson imzalı temel makale
- micro-frontends.org (yeni sekmede açılır) - Birden fazla bağımsız ekiple modern web uygulamaları kurmak için teknikler ve tarifler
- Module Federation Concepts - webpack (yeni sekmede açılır) - Micro frontend’ler arasında çalışma zamanında kod paylaşımı için resmi webpack belgeleri
- module-federation.io (yeni sekmede açılır) - Dinamik tip ipuçları, çalışma zamanı eklenti sistemi ve ileri düzey kalıpları içeren Module Federation 2.0 belgeleri
- single-spa Documentation (yeni sekmede açılır) - Tek bir sayfada birden fazla micro frontend’i orkestre etmek için kullanılan framework
- ModuleFederationPlugin Referansı (yeni sekmede açılır) - webpack ModuleFederationPlugin yapılandırma seçenekleri için tam API referansı
- Web Vitals (yeni sekmede açılır) - Micro frontend başına performans bütçelerinin dayandığı Core Web Vitals tanımları ve eşik değerleri
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.
Bu serideki tüm yazılar
İlgili yazılar
Yönlendirme motoru, analytics toplama ve API Gateway yapılandırması: günlük milyonlarca yönlendirme için performans optimizasyonları ve debugging stratejileri.
aws-cdk · lambda · api-gateway +5
Single Table Design'da DynamoDB throttling'i önleme ve yönetme: partition key tasarımı, write sharding, kapasite modları, DAX caching ve retry pattern'leri.
dynamodb · aws · reliability +4
Modern TypeScript linting ve formatlama araçlarının karşılaştırması: ESLint, Prettier, Biome ve Oxlint için performans ölçümleri, konfigürasyon ve migration.
typescript · code-quality · developer-experience +2
Bun ve Deno'yu AWS Lambda'da custom runtime ile çalıştırma: performans benchmark'ları, maliyet analizi ve production deployment pattern'leri.
lambda · javascript · serverless +2
Observer, Strategy, Command, State ve Mediator TypeScript'te: hangisini dilin kendisi karşılıyor, RxJS, Redux ve XState ne zaman yerini hak ediyor.
typescript · design-patterns · functional-programming +1