İçeriğe atla

Çapraz Platform Micro Frontend'lerde Re.Pack ve Rspack

React Native ve WebView'lar ile mobil micro frontend'ler oluşturma. Çok kanallı uygulamalar için production stratejileri ve performans optimizasyonu.

Ayhan Sipahi Ayhan Sipahi

Mobil öncelikli micro frontend mimarileri, ikinci bir kanalla (web, desktop) karşılaştıklarında değişmeden nadiren ayakta kalır. Mimariyi mobilde çalıştıran varsayımlar (WebView yaşam döngüsü, native köprü, native gesture’lar, kanal başına asset bundle’ları) ya web ve desktop’a hiç eşlenmez ya da kodu paylaşmanın amacını ortadan kaldıran bir performans maliyetine eşlenir. Gerçek çok kanallı micro frontend’ler (mobil, web, desktop Electron) sadece “bir kez yaz, her yere deploy et” değil, bir runtime soyutlama katmanı ve kanala özel kod yollarında bir disiplin gerektirir.

Üç kanalda da ayakta kalan varsayılan şudur: her platform için ayrı bir implementasyonu olan tek bir evrensel köprü arayüzü, bir yetenek eksik olduğunda sessiz kalmayıp hata veren bir yetenek haritası ve her takımın kendi araçlarıyla ürettiği kanal başına bundle’lar. Aşağıdaki her şey bu seçime bağlıdır: neyin paylaşılıp neyin ayrılacağı, tek bir kod tabanını uygulanabilir kılan bundle bölme ve bellek bütçeleri, native köprü disiplini ve üç ayrı sürüm trenini koordine etmeden web, mobil ve Electron’a güncelleme yayınlama modeli.

Mobil Micro Frontend Serisi#

Bu, mobil micro frontend serimizin 3. Kısmı (son):

Yeni başlıyor musunuz? Temeller için Kısım 1 ile başlayın.

İletişim kalıplarına mı ihtiyacınız var? Önce Kısım 2’ye bakın.

Alternatif Çok Kanallı Yaklaşımlar#

Bu varsayılanın karşısında üç ciddi alternatif var. Her birinin bedeli ve hangi durumda daha iyi seçim olduğu aşağıda.

Seçenek 1: Re.Pack Çok Kanallı Mimari#

Re.Pack (yeni sekmede açılır), Module Federation aracılığıyla React Native, web ve masaüstü için birleşik bir yaklaşım sunar:

Örnek kurulum:

// Re.Pack çok kanallı kurulum
// webpack.config.js (paylaşılan konfigürasyon)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        payment: 'payment@http://localhost:3001/remoteEntry.js',
        booking: 'booking@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@shared/platform': { singleton: true }
      }
    })
  ]
};

// Platform abstraction katmanı
// @shared/platform/index.ts
export interface PlatformAPI {
  // Navigasyon
  navigate(screen: string, params?: any): void;
  goBack(): void;

  // Native özellikler
  camera: {
    takePhoto(): Promise<string>;
    selectFromGallery(): Promise<string>;
  };

  // Depolama
  storage: {
    get(key: string): Promise<any>;
    set(key: string, value: any): Promise<void>;
  };

  // Ağ
  network: {
    isOnline(): boolean;
    onNetworkChange(callback: (online: boolean) => void): void;
  };
}

// Platform-specific implementasyonlar
export class ReactNativePlatform implements PlatformAPI {
  // React Native implementasyonu
}

export class WebPlatform implements PlatformAPI {
  // Web implementasyonu
}

export class ElectronPlatform implements PlatformAPI {
  // Electron implementasyonu
}

Tercih edilmeme nedenleri:

  • Karmaşıklık: Mevcut uygulamaların önemli refactoring’ini gerektiriyordu
  • Takım koordinasyonu: Tüm takımların aynı anda benimsemesi gerekiyordu
  • Performans: Mobilde Module Federation overhead’ı
  • Hata ayıklama: Platformlar arası karmaşık stack trace’ler

Ne zaman Re.Pack çok kanallı kullanılır:

  • Sıfırdan yeni bir super app inşa ediyorsanız
  • Tüm takımlar paylaşılan mimaride koordinasyon kurabiliyorsa
  • Platformlar arası gerçek kod paylaşımına ihtiyacınız varsa
  • Performans overhead’ı kabul edilebilirse

Seçenek 2: Rspack Çok Kanallı Build’ler#

Rspack, çok kanallı build’ler için bir başka seçenektir:

// rspack.config.mjs - Çok kanallı konfigürasyon
import { container } from '@rspack/core';

export default {
  entry: './src/index.tsx',
  target: ['web', 'electron-renderer'], // Birden fazla target
  module: {
    rules: [
      {
        test: /\.tsx$/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'typescript',
                tsx: true
              },
              transform: {
                react: {
                  runtime: 'automatic'
                }
              }
            }
          }
        }
      }
    ]
  },
  plugins: [
    new container.ModuleFederationPlugin({
      name: 'micro-frontend',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App.tsx'
      }
    })
  ]
};

// Platform tespiti
const platform = process.env.PLATFORM || 'web';

// Platform'a göre koşullu import'lar
const platformAPI = platform === 'react-native'
  ? require('./platforms/react-native')
  : platform === 'electron'
  ? require('./platforms/electron')
  : require('./platforms/web');

Tercih edilmeme nedenleri:

  • React Native uyumluluğu: Re.Pack 5.x artık Rspack’i destekliyor, ancak bizim değerlendirmemiz React Native desteği sınırlı olan önceki sürümler sırasında yapıldı
  • Build karmaşıklığı: Birden fazla target build’i karmaşıktı
  • Ekosistem olgunluğu: Daha az örnek ve dokümantasyon
  • Takım benimsemesi: Önemli ölçüde yeniden eğitim gerektirecekti

Ne zaman Rspack çok kanallı kullanılır:

  • Sadece web ve masaüstü inşa ediyorsanız
  • Build performansı kritikse
  • Erken benimseyen olmayı göze alabiliyorsanız
  • Takımlarınız Rust tabanlı araçlarla rahatsa

Seçenek 3: Vite Çok Kanallı Mimari#

Vite de çok kanallı build’ler için değerlendirmeye değer:

// vite.config.ts - Çok kanallı kurulum
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode }) => {
  const platform = process.env.PLATFORM || 'web';

  return {
    plugins: [react()],
    build: {
      rollupOptions: {
        external: platform === 'electron' ? ['electron'] : [],
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            platform: [`./src/platforms/${platform}`]
          }
        }
      }
    },
    define: {
      __PLATFORM__: JSON.stringify(platform)
    }
  };
});

// Platform-specific giriş noktaları
// src/platforms/web/index.ts
export class WebPlatform {
  // Web-specific implementasyon
}

// src/platforms/electron/index.ts
export class ElectronPlatform {
  // Electron-specific implementasyon
}

Tercih edilmeme nedenleri:

  • Module Federation: Module Federation v2 artık stabil, ancak değerlendirme sırasında Vite’ın Module Federation’ı hala deneyseldi
  • Production build’ler: Bu kullanım durumu için webpack’ten daha yavaştı
  • Plugin ekosistemi: Özel ihtiyaçlar için daha az plugin
  • Takım aşinalığı: Takımlar webpack ile daha rahattı

Ne zaman Vite çok kanallı kullanılır:

  • Modern web uygulamaları inşa ediyorsanız
  • Geliştirme hızı production optimizasyonundan daha önemliyse
  • Karmaşık Module Federation’a ihtiyacınız yoksa
  • Takımlarınız modern araçları tercih ederse

Seçenek 4: Hibrit Yaklaşım (Önerilen Yol)#

Tüm seçenekler değerlendirildikten sonra, her ortamın güçlü yanlarını birleştiren hibrit bir yaklaşım öne çıkıyor:

Platform Services

Rendering Contexts

Build Systems

Team A: Webpack

Team B: Vite

Team C: Rspack

Micro Frontend App

Platform Detection Layer

Mobile Bridge

Web Bridge

Desktop Bridge

React Native WebView

Browser Window

Electron Renderer

Native Mobile APIs

Web APIs

Electron Main Process

Desktop OS APIs

Shared Business Logic

Platform-Agnostic Services

Bu yaklaşım neden ayakta kalır:

  • Takım özerkliği: Her takım tercih ettiği bundler’ı kullanmayı sürdürür
  • Kademeli geçiş: Takımlar daha iyi araçlara kendi takvimlerinde geçer
  • Risk azaltma: Bir yaklaşım tıkanırsa diğer kanallar yayına çıkmaya devam eder
  • Performans: Her takım kendi build’ini ayarlar

Çok Kanala Geçerken Çıkan Sorunlar#

React Native WebView’ları için kurulmuş bir mobil mimari kendi başına iyi çalışır, ancak web ve masaüstüne genişletildiğinde birkaç sorun ortaya çıkar:

  • Performans: Masaüstü kullanıcıları native düzeyinde performans bekler
  • Navigasyon: Tarayıcı geri butonuna karşı native navigasyon
  • Kimlik doğrulama: Platformlar arası farklı güvenlik modelleri
  • Offline: Mobilin offline çalışması gerekir; web geleneksel olarak buna ihtiyaç duymaz
  • Platform API’ları: Kamera her yerde farklı çalışır

Bunların her biri yukarıdaki diyagramdaki bridge sınırında bir kez yanıtlanır; böylece tek tek micro frontend’lerin aynı sorunu yeniden çözmesi gerekmez.

Platform Tespiti ve Uyarlama#

İlk sorun, runtime ortamını güvenilir biçimde tespit etmek:

// Gerçekten çalışan platform tespiti
export enum Platform {
  REACT_NATIVE_IOS = 'react-native-ios',
  REACT_NATIVE_ANDROID = 'react-native-android',
  WEB_BROWSER = 'web-browser',
  ELECTRON_DESKTOP = 'electron-desktop',
  UNKNOWN = 'unknown'
}

class PlatformDetector {
  private static _platform?: Platform;

  static get platform(): Platform {
    if (this._platform) return this._platform;

    // Önce React Native'i kontrol et
    if (typeof navigator !== 'undefined' &&
        navigator.product === 'ReactNative') {
      // React Native WebView'dayız
      if (window.ReactNativeWebView) {
        // User agent aracılığıyla iOS vs Android tespit et
        const userAgent = navigator.userAgent;
        this._platform = userAgent.includes('iPhone') || userAgent.includes('iPad')
          ? Platform.REACT_NATIVE_IOS
          : Platform.REACT_NATIVE_ANDROID;
      } else {
        this._platform = Platform.UNKNOWN;
      }
    }
    // Electron'u kontrol et
    else if (typeof window !== 'undefined' &&
             window.process?.type === 'renderer') {
      this._platform = Platform.ELECTRON_DESKTOP;
    }
    // Web tarayıcısı olmalı
    else if (typeof window !== 'undefined') {
      this._platform = Platform.WEB_BROWSER;
    }
    else {
      this._platform = Platform.UNKNOWN;
    }

    return this._platform;
  }

  static isMobile(): boolean {
    return this.platform === Platform.REACT_NATIVE_IOS ||
           this.platform === Platform.REACT_NATIVE_ANDROID;
  }

  static isWeb(): boolean {
    return this.platform === Platform.WEB_BROWSER;
  }

  static isDesktop(): boolean {
    return this.platform === Platform.ELECTRON_DESKTOP;
  }

  static hasNativeBridge(): boolean {
    return this.isMobile() || this.isDesktop();
  }
}

Evrensel Bridge Mimarisi#

Birleştirici hamle, her platformun kendi yöntemiyle uyguladığı tek bir bridge arayüzü:

// Evrensel bridge arayüzü
export interface UniversalBridge {
  // Çekirdek mesajlaşma
  request<T>(action: string, payload?: any): Promise<T>;
  emit(action: string, payload?: any): void;
  on(action: string, handler: (payload: any) => void): () => void;

  // Platform yetenekleri
  getCapabilities(): PlatformCapabilities;
  isSupported(feature: string): boolean;
}

export interface PlatformCapabilities {
  hasCamera: boolean;
  hasBiometrics: boolean;
  hasFileSystem: boolean;
  hasNotifications: boolean;
  canGoBack: boolean;
  hasClipboard: boolean;
  canDownload: boolean;
  hasNativeNavigation: boolean;
}

// Platform-specific implementasyonlar
class ReactNativeBridge implements UniversalBridge {
  private bridge: WebViewBridge;

  constructor() {
    this.bridge = window.bridge; // Kısım 2'den
  }

  async request<T>(action: string, payload?: any): Promise<T> {
    return this.bridge.request(action, payload);
  }

  emit(action: string, payload?: any): void {
    this.bridge.emit(action, payload);
  }

  on(action: string, handler: (payload: any) => void): () => void {
    return this.bridge.on(action, handler);
  }

  getCapabilities(): PlatformCapabilities {
    return {
      hasCamera: true,
      hasBiometrics: true,
      hasFileSystem: true,
      hasNotifications: true,
      canGoBack: true,
      hasClipboard: true,
      canDownload: true,
      hasNativeNavigation: true
    };
  }

  isSupported(feature: string): boolean {
    return this.getCapabilities()[feature as keyof PlatformCapabilities] || false;
  }
}

class WebBridge implements UniversalBridge {
  private eventEmitter = new EventTarget();

  async request<T>(action: string, payload?: any): Promise<T> {
    // Web API'ları kullanarak native özellikleri simüle et
    switch (action) {
      case 'camera.takePhoto':
        return this.handleWebCamera(payload) as Promise<T>;
      case 'auth.getToken':
        return this.handleWebAuth() as Promise<T>;
      case 'navigation.goBack':
        return this.handleWebNavigation() as Promise<T>;
      default:
        throw new Error(`Web'de desteklenmeyen eylem: ${action}`);
    }
  }

  emit(action: string, payload?: any): void {
    this.eventEmitter.dispatchEvent(
      new CustomEvent(action, { detail: payload })
    );
  }

  on(action: string, handler: (payload: any) => void): () => void {
    const listener = (event: any) => handler(event.detail);
    this.eventEmitter.addEventListener(action, listener);

    return () => {
      this.eventEmitter.removeEventListener(action, listener);
    };
  }

  private async handleWebCamera(options: any): Promise<any> {
    // HTML5 kamera API'sını kullan
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1920 },
        height: { ideal: 1080 }
      }
    });

    return new Promise((resolve, reject) => {
      const video = document.createElement('video');
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d')!;

      video.srcObject = stream;
      video.play();

      video.onloadedmetadata = () => {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;

        // Kamera yakalama UI'sını simüle et
        const captureButton = document.createElement('button');
        captureButton.textContent = 'Yakala';
        captureButton.onclick = () => {
          ctx.drawImage(video, 0, 0);
          const dataUri = canvas.toDataURL('image/jpeg', options?.quality || 0.8);

          stream.getTracks().forEach(track => track.stop());
          document.body.removeChild(video);
          document.body.removeChild(captureButton);

          resolve({
            uri: dataUri,
            width: canvas.width,
            height: canvas.height
          });
        };

        document.body.appendChild(video);
        document.body.appendChild(captureButton);
      };
    });
  }

  private async handleWebAuth(): Promise<any> {
    // Web kimlik doğrulaması için localStorage/sessionStorage kullan
    const token = localStorage.getItem('auth_token');
    if (!token) {
      throw new Error('Kimlik doğrulama token bulunamadı');
    }

    return {
      token,
      expiresAt: Date.now() + 3600000 // 1 saat
    };
  }

  private async handleWebNavigation(): Promise<boolean> {
    if (window.history.length > 1) {
      window.history.back();
      return true;
    }
    return false;
  }

  getCapabilities(): PlatformCapabilities {
    return {
      hasCamera: !!navigator.mediaDevices?.getUserMedia,
      hasBiometrics: false, // Tarayıcılarda mevcut değil
      hasFileSystem: false, // Tarayıcılarda sınırlı
      hasNotifications: 'Notification' in window,
      canGoBack: true,
      hasClipboard: !!navigator.clipboard,
      canDownload: true,
      hasNativeNavigation: false
    };
  }

  isSupported(feature: string): boolean {
    return this.getCapabilities()[feature as keyof PlatformCapabilities] || false;
  }
}

class ElectronBridge implements UniversalBridge {
  async request<T>(action: string, payload?: any): Promise<T> {
    // Ana process ile iletişim için Electron'un IPC'sini kullan
    return window.electronAPI.invoke(action, payload);
  }

  emit(action: string, payload?: any): void {
    window.electronAPI.send(action, payload);
  }

  on(action: string, handler: (payload: any) => void): () => void {
    const listener = (_: any, ...args: any[]) => handler(...args);
    window.electronAPI.on(action, listener);

    return () => {
      window.electronAPI.removeListener(action, listener);
    };
  }

  getCapabilities(): PlatformCapabilities {
    return {
      hasCamera: true,
      hasBiometrics: false, // Native modül gerekir
      hasFileSystem: true,
      hasNotifications: true,
      canGoBack: true,
      hasClipboard: true,
      canDownload: true,
      hasNativeNavigation: true
    };
  }

  isSupported(feature: string): boolean {
    return this.getCapabilities()[feature as keyof PlatformCapabilities] || false;
  }
}

// Bridge factory
export class BridgeFactory {
  static create(): UniversalBridge {
    const platform = PlatformDetector.platform;

    switch (platform) {
      case Platform.REACT_NATIVE_IOS:
      case Platform.REACT_NATIVE_ANDROID:
        return new ReactNativeBridge();

      case Platform.ELECTRON_DESKTOP:
        return new ElectronBridge();

      case Platform.WEB_BROWSER:
        return new WebBridge();

      default:
        throw new Error(`Desteklenmeyen platform: ${platform}`);
    }
  }
}

Adaptif UI Bileşenleri#

Farklı platformlar farklı UX kalıplarına ihtiyaç duyar. Adaptif bileşenler bunu çözer:

// Adaptif buton bileşeni
interface AdaptiveButtonProps {
  title: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

export function AdaptiveButton({
  title,
  onPress,
  variant = 'primary',
  disabled = false
}: AdaptiveButtonProps) {
  const platform = PlatformDetector.platform;

  // Platform-specific stiller
  const getButtonStyles = () => {
    const baseStyles = 'px-4 py-2 rounded font-medium transition-colors';

    if (disabled) {
      return `${baseStyles} bg-gray-300 text-gray-500 cursor-not-allowed`;
    }

    const variantStyles = variant === 'primary'
      ? 'bg-blue-600 text-white hover:bg-blue-700'
      : 'bg-gray-200 text-gray-900 hover:bg-gray-300';

    // Platform-specific modifikasyonlar
    switch (platform) {
      case Platform.REACT_NATIVE_IOS:
        // iOS tarzı buton
        return `${baseStyles} ${variantStyles} shadow-sm`;

      case Platform.REACT_NATIVE_ANDROID:
        // Material Design stili
        return `${baseStyles} ${variantStyles} shadow-md elevation-2`;

      case Platform.WEB_BROWSER:
        // Web'e optimize edilmiş
        return `${baseStyles} ${variantStyles} focus:outline-none focus:ring-2 focus:ring-blue-500`;

      case Platform.ELECTRON_DESKTOP:
        // Masaüstü uygulama stili
        return `${baseStyles} ${variantStyles} text-sm`;

      default:
        return `${baseStyles} ${variantStyles}`;
    }
  };

  const handleClick = () => {
    if (disabled) return;

    // Mobile'da haptik geri bildirim ekle
    if (PlatformDetector.isMobile()) {
      bridge.emit('haptic.impact', { style: 'medium' });
    }

    onPress();
  };

  return (
    <button
      className={getButtonStyles()}
      onClick={handleClick}
      disabled={disabled}
      // Erişilebilirlik
      role="button"
      aria-label={title}
    >
      {title}
    </button>
  );
}

// Adaptif layout bileşenleri
export function AdaptiveLayout({ children }: { children: React.ReactNode }) {
  const platform = PlatformDetector.platform;

  const getLayoutClass = () => {
    switch (platform) {
      case Platform.REACT_NATIVE_IOS:
      case Platform.REACT_NATIVE_ANDROID:
        // Mobile: tam yükseklik, güvenli alanlar
        return 'min-h-screen pt-safe pb-safe px-4';

      case Platform.WEB_BROWSER:
        // Web: responsive, maksimum genişlik
        return 'min-h-screen max-w-4xl mx-auto px-4 py-8';

      case Platform.ELECTRON_DESKTOP:
        // Masaüstü: kompakt, pencere-farkında
        return 'h-screen p-6 overflow-auto';

      default:
        return 'min-h-screen p-4';
    }
  };

  return (
    <div className={getLayoutClass()}>
      {children}
    </div>
  );
}

Performans Optimizasyonu Stratejileri#

Aynı kod tabanını tüm platformlarda çalıştırmak, tek platformlu geliştirmede göze çarpmayan performans darboğazlarını ortaya çıkarır:

Platforma Göre Kod Bölme#

// Platforma duyarlı dinamik import'lar
class PlatformModuleLoader {
  private moduleCache = new Map<string, any>();

  async loadPlatformModule<T>(moduleName: string): Promise<T> {
    const cacheKey = `${moduleName}-${PlatformDetector.platform}`;

    if (this.moduleCache.has(cacheKey)) {
      return this.moduleCache.get(cacheKey);
    }

    let module: T;

    try {
      // Önce platform-specific modülü dene
      const platformModule = await this.loadPlatformSpecific<T>(moduleName);
      module = platformModule;
    } catch (error) {
      // Generic modüle geri dön
      console.warn(`Platform modülü ${moduleName} bulunamadı, generic kullanılıyor`);
      module = await this.loadGeneric<T>(moduleName);
    }

    this.moduleCache.set(cacheKey, module);
    return module;
  }

  private async loadPlatformSpecific<T>(moduleName: string): Promise<T> {
    const platform = PlatformDetector.platform;

    switch (platform) {
      case Platform.REACT_NATIVE_IOS:
        return import(`./modules/${moduleName}/ios`);
      case Platform.REACT_NATIVE_ANDROID:
        return import(`./modules/${moduleName}/android`);
      case Platform.WEB_BROWSER:
        return import(`./modules/${moduleName}/web`);
      case Platform.ELECTRON_DESKTOP:
        return import(`./modules/${moduleName}/electron`);
      default:
        throw new Error(`${platform} için platform-specific modül yok`);
    }
  }

  private async loadGeneric<T>(moduleName: string): Promise<T> {
    return import(`./modules/${moduleName}/index`);
  }
}

// Kullanım
const moduleLoader = new PlatformModuleLoader();

async function initializeApp() {
  // Platform-optimize edilmiş modülleri yükle
  const analytics = await moduleLoader.loadPlatformModule('analytics');
  const storage = await moduleLoader.loadPlatformModule('storage');
  const networking = await moduleLoader.loadPlatformModule('networking');

  // Platform-specific implementasyonlarla başlat
  await analytics.initialize();
  await storage.initialize();
  await networking.initialize();
}

Platformlar Arası Bellek Yönetimi#

Bellek kısıtları platformdan platforma keskin biçimde değişir:

// Adaptif bellek yönetimi
class MemoryManager {
  private memoryLimit: number;
  private memoryWarningThreshold: number;
  private cache = new Map<string, { data: any; timestamp: number; size: number }>();
  private totalCacheSize = 0;

  constructor() {
    // Platform'a göre limitleri ayarla
    const platform = PlatformDetector.platform;

    switch (platform) {
      case Platform.REACT_NATIVE_IOS:
      case Platform.REACT_NATIVE_ANDROID:
        // Mobile: muhafazakar limitler
        this.memoryLimit = 50 * 1024 * 1024; // 50MB
        this.memoryWarningThreshold = 40 * 1024 * 1024; // 40MB
        break;

      case Platform.WEB_BROWSER:
        // Web: orta limitler
        this.memoryLimit = 100 * 1024 * 1024; // 100MB
        this.memoryWarningThreshold = 80 * 1024 * 1024; // 80MB
        break;

      case Platform.ELECTRON_DESKTOP:
        // Masaüstü: cömert limitler
        this.memoryLimit = 200 * 1024 * 1024; // 200MB
        this.memoryWarningThreshold = 150 * 1024 * 1024; // 150MB
        break;

      default:
        this.memoryLimit = 50 * 1024 * 1024;
        this.memoryWarningThreshold = 40 * 1024 * 1024;
    }

    // Bellek baskısı izlemeyi kur
    this.setupMemoryMonitoring();
  }

  set(key: string, data: any): void {
    const serialized = JSON.stringify(data);
    const size = new Blob([serialized]).size;

    // Önce bellek boşaltmamız gerekip gerekmediğini kontrol et
    if (this.totalCacheSize + size > this.memoryLimit) {
      this.freeMemory(size);
    }

    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      size
    });

    this.totalCacheSize += size;
  }

  get(key: string): any {
    const item = this.cache.get(key);
    if (!item) return null;

    // LRU için timestamp'ı güncelle
    item.timestamp = Date.now();
    return item.data;
  }

  private freeMemory(requiredSize: number): void {
    // Timestamp'a göre sırala (LRU)
    const entries = Array.from(this.cache.entries())
      .sort(([, a], [, b]) => a.timestamp - b.timestamp);

    let freed = 0;

    for (const [key, item] of entries) {
      this.cache.delete(key);
      this.totalCacheSize -= item.size;
      freed += item.size;

      if (freed >= requiredSize || this.totalCacheSize < this.memoryWarningThreshold) {
        break;
      }
    }

    console.log(`[MemoryManager] ${freed} byte boşaltıldı`);
  }

  private setupMemoryMonitoring(): void {
    // Her 30 saniyede bellek kullanımını izle
    setInterval(() => {
      if (this.totalCacheSize > this.memoryWarningThreshold) {
        console.warn(`[MemoryManager] Bellek kullanımı yüksek: ${this.totalCacheSize} byte`);

        // Cache'in %25'ini boşalt
        this.freeMemory(this.totalCacheSize * 0.25);
      }
    }, 30000);

    // Platform-specific bellek uyarılarına tepki ver
    if (PlatformDetector.isMobile()) {
      // Mobil uygulamalar bellek uyarıları alabilir
      bridge.on('memory.warning', () => {
        console.warn('[MemoryManager] Bellek uyarısı alındı, cache temizleniyor');
        this.clearAll();
      });
    }
  }

  clearAll(): void {
    this.cache.clear();
    this.totalCacheSize = 0;
  }

  getStats(): {
    totalSize: number;
    itemCount: number;
    limit: number;
    utilizationPercent: number;
  } {
    return {
      totalSize: this.totalCacheSize,
      itemCount: this.cache.size,
      limit: this.memoryLimit,
      utilizationPercent: (this.totalCacheSize / this.memoryLimit) * 100
    };
  }
}

Ağ Optimizasyonu#

Ağ davranışı platformlar arasında önemli ölçüde değişir:

// Adaptif ağ stratejisi
class NetworkManager {
  private retryDelays: number[];
  private timeoutMs: number;
  private maxConcurrentRequests: number;
  private activeRequests = new Set<string>();
  private requestQueue: Array<() => Promise<any>> = [];

  constructor() {
    const platform = PlatformDetector.platform;

    switch (platform) {
      case Platform.REACT_NATIVE_IOS:
      case Platform.REACT_NATIVE_ANDROID:
        // Mobile: muhafazakar ayarlar
        this.retryDelays = [1000, 2000, 5000];
        this.timeoutMs = 15000;
        this.maxConcurrentRequests = 3;
        break;

      case Platform.WEB_BROWSER:
        // Web: orta ayarlar
        this.retryDelays = [500, 1000, 2000];
        this.timeoutMs = 10000;
        this.maxConcurrentRequests = 6;
        break;

      case Platform.ELECTRON_DESKTOP:
        // Masaüstü: agresif ayarlar
        this.retryDelays = [200, 500, 1000];
        this.timeoutMs = 5000;
        this.maxConcurrentRequests = 10;
        break;

      default:
        this.retryDelays = [1000, 2000, 5000];
        this.timeoutMs = 10000;
        this.maxConcurrentRequests = 3;
    }
  }

  async request(url: string, options: RequestInit = {}): Promise<Response> {
    // Mobile için request kuyruğu uygula
    if (PlatformDetector.isMobile() &&
        this.activeRequests.size >= this.maxConcurrentRequests) {
      return this.queueRequest(() => this.executeRequest(url, options));
    }

    return this.executeRequest(url, options);
  }

  private async executeRequest(url: string, options: RequestInit): Promise<Response> {
    const requestId = `${url}-${Date.now()}`;
    this.activeRequests.add(requestId);

    try {
      return await this.retryRequest(url, options);
    } finally {
      this.activeRequests.delete(requestId);
      this.processQueue();
    }
  }

  private async retryRequest(url: string, options: RequestInit): Promise<Response> {
    let lastError: Error;

    for (let attempt = 0; attempt <= this.retryDelays.length; attempt++) {
      try {
        // Request'e timeout ekle
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);

        const response = await fetch(url, {
          ...options,
          signal: controller.signal
        });

        clearTimeout(timeoutId);

        // 4xx hatalarında tekrar deneme yapma (429 hariç)
        if (response.status >= 400 && response.status < 500 && response.status !== 429) {
          return response;
        }

        if (response.ok || response.status === 429) {
          return response;
        }

        throw new Error(`HTTP ${response.status}: ${response.statusText}`);

      } catch (error) {
        lastError = error as Error;

        // Abort durumunda tekrar deneme yapma
        if (error.name === 'AbortError') {
          throw error;
        }

        // Tekrar denemeden önce bekle
        if (attempt < this.retryDelays.length) {
          console.warn(`Request denemesi ${attempt + 1} başarısız, ${this.retryDelays[attempt]}ms içinde tekrar deneniyor`);
          await this.delay(this.retryDelays[attempt]);
        }
      }
    }

    throw lastError!;
  }

  private async queueRequest<T>(requestFn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.requestQueue.push(async () => {
        try {
          const result = await requestFn();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
    });
  }

  private processQueue(): void {
    while (this.requestQueue.length > 0 &&
           this.activeRequests.size < this.maxConcurrentRequests) {
      const nextRequest = this.requestQueue.shift();
      nextRequest?.();
    }
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Offline Destek ve Senkronizasyon#

En zor kısımlardan biri, tüm platformlarda aynı şekilde davranan offline desteği:

// Evrensel offline destek
class OfflineManager {
  private isOnline = navigator.onLine;
  private syncQueue: Array<{
    id: string;
    action: string;
    payload: any;
    timestamp: number;
    attempts: number;
  }> = [];
  private storage: Storage;
  private maxRetries = 5;

  constructor() {
    // Platform-uygun storage kullan
    this.storage = PlatformDetector.isMobile()
      ? this.createMobileStorage()
      : localStorage;

    this.setupNetworkMonitoring();
    this.loadQueueFromStorage();
  }

  private createMobileStorage(): Storage {
    // React Native WebView'da, kalıcı storage için bridge kullanmamız gerekiyor
    return {
      getItem: async (key: string): Promise<string | null> => {
        try {
          return await bridge.request('storage.getItem', { key });
        } catch {
          return null;
        }
      },
      setItem: async (key: string, value: string): Promise<void> => {
        await bridge.request('storage.setItem', { key, value });
      },
      removeItem: async (key: string): Promise<void> => {
        await bridge.request('storage.removeItem', { key });
      },
      clear: async (): Promise<void> => {
        await bridge.request('storage.clear');
      }
    } as any;
  }

  private setupNetworkMonitoring(): void {
    // Web/Masaüstü ağ izleme
    if (typeof window !== 'undefined') {
      window.addEventListener('online', () => {
        this.isOnline = true;
        this.processSyncQueue();
      });

      window.addEventListener('offline', () => {
        this.isOnline = false;
      });
    }

    // Bridge aracılığıyla mobil ağ izleme
    if (PlatformDetector.isMobile()) {
      bridge.on('network.statusChanged', ({ isOnline }: { isOnline: boolean }) => {
        this.isOnline = isOnline;
        if (isOnline) {
          this.processSyncQueue();
        }
      });
    }
  }

  async queueForSync(action: string, payload: any): Promise<void> {
    const queueItem = {
      id: this.generateId(),
      action,
      payload,
      timestamp: Date.now(),
      attempts: 0
    };

    this.syncQueue.push(queueItem);
    await this.saveQueueToStorage();

    // Online'sa hemen senkronize etmeye çalış
    if (this.isOnline) {
      this.processSyncQueue();
    }
  }

  private async processSyncQueue(): Promise<void> {
    if (!this.isOnline || this.syncQueue.length === 0) {
      return;
    }

    const itemsToProcess = [...this.syncQueue];

    for (const item of itemsToProcess) {
      try {
        await this.syncItem(item);

        // Başarıda kuyruktan çıkar
        this.syncQueue = this.syncQueue.filter(q => q.id !== item.id);

      } catch (error) {
        console.error(`${item.action} için sync başarısız:`, error);

        item.attempts++;

        // Maksimum deneme aşılırsa çıkar
        if (item.attempts >= this.maxRetries) {
          console.error(`${item.action} için maksimum deneme aşıldı, kuyruktan çıkarılıyor`);
          this.syncQueue = this.syncQueue.filter(q => q.id !== item.id);
        }
      }
    }

    await this.saveQueueToStorage();
  }

  private async syncItem(item: any): Promise<void> {
    // Kuyruktaki eylemi sunucuya gönder
    const response = await fetch('/api/sync', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${await this.getAuthToken()}`
      },
      body: JSON.stringify({
        action: item.action,
        payload: item.payload,
        timestamp: item.timestamp
      })
    });

    if (!response.ok) {
      throw new Error(`Sync başarısız: ${response.status}`);
    }
  }

  private async getAuthToken(): Promise<string> {
    if (PlatformDetector.hasNativeBridge()) {
      const { token } = await bridge.request('auth.getToken');
      return token;
    } else {
      return localStorage.getItem('auth_token') || '';
    }
  }

  private async saveQueueToStorage(): Promise<void> {
    const serialized = JSON.stringify(this.syncQueue);
    await this.storage.setItem('sync_queue', serialized);
  }

  private async loadQueueFromStorage(): Promise<void> {
    try {
      const serialized = await this.storage.getItem('sync_queue');
      if (serialized) {
        this.syncQueue = JSON.parse(serialized);
      }
    } catch (error) {
      console.error('Sync kuyruğu yüklenemedi:', error);
      this.syncQueue = [];
    }
  }

  private generateId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  // Public API
  isOnlineState(): boolean {
    return this.isOnline;
  }

  getPendingActions(): number {
    return this.syncQueue.length;
  }

  async forceSync(): Promise<void> {
    await this.processSyncQueue();
  }
}

Production İzleme ve Analitik#

Platformlar arası performansı izlemek, platforma duyarlı kaynaklardan beslenen tek bir toplayıcı gerektirir:

// Evrensel analitik sistemi
class UniversalAnalytics {
  private sessionId: string;
  private userId?: string;
  private deviceInfo: DeviceInfo;
  private performanceBuffer: PerformanceEntry[] = [];

  constructor() {
    this.sessionId = this.generateSessionId();
    this.deviceInfo = this.collectDeviceInfo();
    this.setupPerformanceMonitoring();
  }

  private collectDeviceInfo(): DeviceInfo {
    const platform = PlatformDetector.platform;

    return {
      platform,
      userAgent: navigator.userAgent,
      screenWidth: window.screen?.width || 0,
      screenHeight: window.screen?.height || 0,
      devicePixelRatio: window.devicePixelRatio || 1,
      // Platform-specific bilgi
      ...this.getPlatformSpecificInfo()
    };
  }

  private getPlatformSpecificInfo(): Partial<DeviceInfo> {
    const platform = PlatformDetector.platform;

    if (PlatformDetector.isMobile()) {
      // Native bridge'den cihaz bilgisi al
      return {
        deviceModel: 'mobile-device', // Bridge'den gelecek
        osVersion: 'unknown', // Bridge'den gelecek
        appVersion: '1.0.0' // Bridge'den gelecek
      };
    }

    return {
      browserName: this.getBrowserName(),
      browserVersion: this.getBrowserVersion()
    };
  }

  private setupPerformanceMonitoring(): void {
    // Web Vitals'ı izle
    this.observeWebVitals();

    // Özel performans metriklerini izle
    this.observeCustomMetrics();

    // Platform-specific izleme
    if (PlatformDetector.isMobile()) {
      this.setupMobileMonitoring();
    }
  }

  private observeWebVitals(): void {
    // Core Web Vitals
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          this.trackPerformance(entry.name, entry.duration, {
            type: entry.entryType,
            startTime: entry.startTime
          });
        });
      });

      try {
        observer.observe({ entryTypes: ['measure', 'navigation', 'paint'] });
      } catch (error) {
        console.warn('Performance observer desteklenmiyor:', error);
      }
    }
  }

  private observeCustomMetrics(): void {
    // Micro frontend yükleme zamanlarını takip et
    this.trackMicroFrontendLoads();

    // Bridge mesaj performansını takip et
    this.trackBridgePerformance();

    // Bellek kullanımını takip et
    this.trackMemoryUsage();
  }

  private trackMicroFrontendLoads(): void {
    // Micro frontend'ler ne zaman görünür hale geldiğini izle
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const loadTime = performance.now();

          this.track('micro_frontend_visible', {
            microFrontendId: entry.target.id,
            loadTime,
            visibilityRatio: entry.intersectionRatio
          });
        }
      });
    });

    // Tüm micro frontend container'larını gözlemle
    document.querySelectorAll('[data-micro-frontend]').forEach((element) => {
      observer.observe(element);
    });
  }

  private trackBridgePerformance(): void {
    // Performansı takip etmek için bridge request'lerini sarla
    const originalRequest = bridge.request;

    bridge.request = async (action: string, payload: any) => {
      const startTime = performance.now();

      try {
        const result = await originalRequest.call(bridge, action, payload);
        const duration = performance.now() - startTime;

        this.trackPerformance('bridge_request', duration, {
          action,
          success: true,
          payloadSize: JSON.stringify(payload).length
        });

        return result;
      } catch (error) {
        const duration = performance.now() - startTime;

        this.trackPerformance('bridge_request', duration, {
          action,
          success: false,
          error: error.message
        });

        throw error;
      }
    };
  }

  private trackMemoryUsage(): void {
    if ('memory' in performance) {
      setInterval(() => {
        const memInfo = (performance as any).memory;

        this.track('memory_usage', {
          usedJSHeapSize: memInfo.usedJSHeapSize,
          totalJSHeapSize: memInfo.totalJSHeapSize,
          jsHeapSizeLimit: memInfo.jsHeapSizeLimit,
          utilizationPercent: (memInfo.usedJSHeapSize / memInfo.jsHeapSizeLimit) * 100
        });
      }, 30000); // Her 30 saniye
    }
  }

  private setupMobileMonitoring(): void {
    // Native bridge hatalarını takip et
    bridge.on('error', (error: any) => {
      this.track('bridge_error', {
        error: error.message,
        code: error.code,
        platform: PlatformDetector.platform
      });
    });

    // Uygulama durum değişikliklerini takip et
    bridge.on('app_state_change', (state: string) => {
      this.track('app_state_change', {
        state,
        sessionDuration: Date.now() - this.getSessionStartTime()
      });
    });
  }

  // Public API
  track(event: string, properties: Record<string, any> = {}): void {
    const eventData = {
      event,
      properties: {
        ...properties,
        sessionId: this.sessionId,
        userId: this.userId,
        platform: this.deviceInfo.platform,
        timestamp: Date.now()
      },
      deviceInfo: this.deviceInfo
    };

    // Analitik servisine gönder
    this.sendAnalytics(eventData);
  }

  trackPerformance(metric: string, value: number, properties: Record<string, any> = {}): void {
    this.track('performance_metric', {
      metric,
      value,
      ...properties
    });
  }

  setUserId(userId: string): void {
    this.userId = userId;
  }

  private async sendAnalytics(data: any): Promise<void> {
    try {
      // Güvenilir teslimat için offline manager kullan
      await offlineManager.queueForSync('analytics', data);
    } catch (error) {
      console.error('Analitik gönderme başarısız:', error);
    }
  }

  private generateSessionId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  private getSessionStartTime(): number {
    return parseInt(this.sessionId.split('-')[0]);
  }

  private getBrowserName(): string {
    const userAgent = navigator.userAgent;
    if (userAgent.includes('Chrome')) return 'Chrome';
    if (userAgent.includes('Firefox')) return 'Firefox';
    if (userAgent.includes('Safari')) return 'Safari';
    if (userAgent.includes('Edge')) return 'Edge';
    return 'Unknown';
  }

  private getBrowserVersion(): string {
    // Basitleştirilmiş versiyon tespiti
    const userAgent = navigator.userAgent;
    const match = userAgent.match(/(?:Chrome|Firefox|Safari|Edge)\/([0-9.]+)/);
    return match ? match[1] : 'Unknown';
  }
}

interface DeviceInfo {
  platform: Platform;
  userAgent: string;
  screenWidth: number;
  screenHeight: number;
  devicePixelRatio: number;
  deviceModel?: string;
  osVersion?: string;
  appVersion?: string;
  browserName?: string;
  browserVersion?: string;
}

interface PerformanceEntry {
  name: string;
  duration: number;
  startTime: number;
  type: string;
}

Sık Yapılan Hatalar#

Bu mimaride tekrar eden dört hata var ve dördünü de önlemek, sonradan düzeltmekten ucuz:

  1. Kanal başına performans bütçesinin olmaması: CI’da zorunlu kılınan bir bütçe yoksa, paylaşılan bundle’lar WebView kanalı üründeki en yavaş yüzey olana kadar büyür. Bütçeyi ikinci kanal yayına çıkmadan önce belirleyin. Sonradan eklemek, kullanıcıların eline çoktan geçmiş özellikleri yeniden pazarlık etmek demektir.

  2. Yalnızca geliştirdiğiniz kanalı test etmek: Tarayıcıda çalışan bir bridge eylemi, native handler hiç kaydedilmediği için WebView’da sessizce başarısız olabilir. Aynı sözleşme testlerini web fallback’i dahil her bridge implementasyonuna karşı çalıştırın.

  3. İzlemeyi yayına çıktıktan sonra eklemek: Bridge gecikmesi, senkronizasyon kuyruğu derinliği ve kanal başına hata oranları bridge’in kendi içinde ölçülmelidir. Sonradan eklendiğinde, tam da yakalaması gereken platforma özel arızaları kaçırır.

  4. Her platforma tek bir bellek bütçesi vermek: Masaüstü renderer’ına göre boyutlandırılmış bir cache, orta seviye bir Android cihazda WebView’ın sonlandırılmasına yol açar. Belleği baştan kanal başına bütçeleyin ve çökme raporunu beklemek yerine platformun bellek uyarısı sinyaline uyun.

Çok Kanallı Micro Frontend’ler Ne Zaman Mantıklı#

Bu mimari şu durumlarda mantıklıdır:

  • Sınırlı kaynaklarla birden fazla platformu desteklemeniz gerekiyorsa
  • Takımların deployment bağımsızlığına ihtiyacı varsa
  • Yeniden kullanılacak önemli mevcut web uygulamalarınız varsa
  • Platformlar arası tutarlılık maksimum performanstan daha önemliyse
  • Platform-specific optimizasyona yatırım yapabilirsiniz

Şu durumlarda ideal değildir:

  • Performans kesinlikle kritikse (oyunlar, video düzenleme, vb.)
  • Deneyimin merkezinde platform-specific özellikleriniz varsa
  • Takım boyutu koordinasyonun darboğaz olmayacağı kadar küçükse
  • Basit bir uygulama inşa ediyorsanız

Sonuç#

Arkasında kanal başına bundle’lar bulunan evrensel bir bridge, birden fazla kanalın aynı ürün mantığını paylaştığı ve arkalarındaki takımların ayrı takvimlerle yayına çıktığı durumlarda başlanacak varsayılandır. Deneyimin tamamını tek bir kanal taşıyorsa bu varsayılanı bırakın: tek platformlu bir üründe ya da kare düzeyinde performansın sonucu belirlediği bir işte, aynı anda dört runtime’da doğru kalmak zorunda olan bir soyutlama yerine native kod daha iyi sonuç verir. Şimdi başlıyorsanız önce yetenek haritasını tanımlayın ve altındaki platforma özel her kararı bu haritanın bildirdiklerinden türetin.

Kaynaklar#

React Native ile Mobil Micro Frontend'ler

React Native, Expo ve WebView'lar kullanarak mobil micro frontend'ler oluşturma üzerine 3 bölümlük kapsamlı seri. Mimari, iletişim pattern'leri ve production optimizasyonu dahil.

İlerleme 3/3 yazı tamamlandı

Bu serideki tüm yazılar

İlgili yazılar