Skip to content
~/sph.sh

Mobil Micro Frontend'lerde WebView İletişim Pattern'leri

React Native ve WebView'lar arasında güvenli ve verimli iletişim kurma. Postmessage, bridge pattern'leri ve production deneyimleri.

Mobil micro frontend mimarimizi devreye aldıktan üç ay sonra bir duvara çarptık. WebView-native iletişimimiz debugging kâbusu haline geliyordu. Mesajlar kayboluyordu, platformlar arası tipler eşleşmiyordu, ve production'da WebView'ların içinde neler olduğunu takip edemiyorduk. Bu yazıda PostMessage tabanlı bridge pattern'imizi, type-safe iletişim katmanını ve production'dan öğrendiklerimizi paylaşıyorum.

Zorluklar kritik bir ürün lansmanı sırasında ortaya çıktı. Native kimlik doğrulama ve WebView checkout'u arasında bölünmüş ödeme akışımız, Android kullanıcılarının 15%'i için başarısız olmaya başladı. WebView ready-state'i doğrulamadan mesaj göndermek yaygın bir tuzak; ready-state polling veya event listener ile çözülebilir. Ana sebep? Yalnızca daha yavaş JavaScript motorlarına sahip cihazlarda görülen mesaj geçiş sistemimizde bir race condition. WebView hazır olmadan gönderilen mesajlar kayboluyordu. İşte iletişim katmanımızı sıfırdan nasıl yeniden inşa ettiğimiz ve production WebView iletişim sistemimizden neler öğrendiğimiz. TypeScript ile type-safe bridge'ler ve ready-state polling pattern'i bu sorunları çözdü—aşağıda uygulanabilir kod örnekleri bulacaksın. Mesaj sıralaması ve timeout mekanizmaları özellikle önemli; yavaş cihazlarda WebView inject edilmeden önce gönderilen komutlar sessizce kaybolur.

Mobil Micro Frontend Serisi

Bu, mobil micro frontend serimizin 2. bölümü. PostMessage, bridge pattern'leri ve production deneyimleriyle devam ediyoruz:

1. Bölümü okumadınız mı? Mimari temeller için oradan başlayın. Production için hazır mısınız? Optimizasyon stratejileri için 3. Bölüm'e geçin.

Alternatif İletişim Yaklaşımları

WebView iletişim çözümümüze dalmadan önce, değerlendirdiğimiz alternatif yaklaşımları ve neden mevcut sistemimizi seçtiğimizi paylaşayım. PostMessage, Re.Pack Module Federation ve custom URL scheme'leri inceledik; her birinin trade-off'ları vardı.

Seçenek 1: Re.Pack Module Federation İletişimi

Re.Pack, Module Federation aracılığıyla farklı bir iletişim modeli sunar. Mesaj geçirme yerine, native ve web bağlamları arasında doğrudan modül paylaşımına olanak tanır.

Denediğimiz şey:

typescript
// Re.Pack Module Federation kurulumu// webpack.config.js (host app)const { ModuleFederationPlugin } = require('@module-federation/nextjs-mf');
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 },        // Paylaşılan iletişim katmanı        '@shared/bridge': { singleton: true }      }    })  ]};
// Paylaşılan bridge modülü// @shared/bridge/index.tsexport interface NativeBridge {  getAuthToken(): Promise<string>;  openCamera(): Promise<string>;  navigate(screen: string, params?: any): void;}
// Micro frontend içindeimport { NativeBridge } from '@shared/bridge';
const PaymentComponent = () => {  const handlePayment = async () => {    // Mesaj geçirme yerine doğrudan fonksiyon çağrısı    const token = await NativeBridge.getAuthToken();    const photo = await NativeBridge.openCamera();
    // Native verilerle ödeme işlemi    await processPayment(token, photo);  };
  return <button onClick={handlePayment}>Öde</button>;};

Neden seçmedik:

  • Karmaşıklık: Tüm ekiplerin aynı anda Module Federation'ı benimsemeleri gerekiyordu; mevcut postMessage tabanlı sistemle kademeli geçiş zordu.
  • Type safety: Paylaşılan modüllerin ayrı bir package'da olması gerekiyordu; bu da build pipeline'ını karmaşıklaştırırdı.
  • Versioning: Modül versiyon çakışmaları debug etmesi zordu
  • Performans: İlk bundle boyutu önemli ölçüde artıyordu
  • Debugging: Stack trace'ler birden fazla context'e yayılıyordu

Re.Pack Module Federation'ı ne zaman kullanmalı:

  • Gerçek bir super app inşa ediyorsanız
  • Tüm ekipler paylaşılan modüller üzerinde koordine olabiliyorsa
  • Context'ler arası doğrudan fonksiyon çağrılarına ihtiyacınız varsa
  • Performans overhead'i kabul edilebilirse

Seçenek 2: Rspack Module Federation

İletişim için Rspack'in Module Federation'ı ile de deneme yaptık:

typescript
// rspack.config.mjsexport default {  entry: './src/index.tsx',  plugins: [    new ModuleFederationPlugin({      name: 'micro-frontend',      filename: 'remoteEntry.js',      exposes: {        './App': './src/App.tsx',        './Bridge': './src/bridge.ts'      },      shared: {        react: { singleton: true },        'react-dom': { singleton: true }      }    })  ]};
// Bridge implementasyonu// src/bridge.tsexport class RspackBridge {  private static instance: RspackBridge;
  static getInstance(): RspackBridge {    if (!RspackBridge.instance) {      RspackBridge.instance = new RspackBridge();    }    return RspackBridge.instance;  }
  async request<T>(action: string, payload?: any): Promise<T> {    // Rspack'in built-in iletişimini kullan    return new Promise((resolve, reject) => {      const timeout = setTimeout(() => {        reject(new Error(`Request ${action} timed out`));      }, 10000);
      // Rspack-spesifik iletişim      window.__RSPACK_MODULE_FEDERATION__.request(action, payload)        .then((result: T) => {          clearTimeout(timeout);          resolve(result);        })        .catch((error: any) => {          clearTimeout(timeout);          reject(error);        });    });  }}

Neden seçmedik:

  • React Native uyumluluğu: Rspack'in native React Native desteği yok
  • Ekosistem olgunluğu: Daha az debugging aracı ve örnek
  • Ekip adaptasyonu: Önemli ölçüde yeniden eğitim gerektirecekti
  • Production kararlılığı: Zamanımız için çok yeni

Rspack Module Federation'ı ne zaman kullanmalı:

  • Yalnızca web micro frontend'leri inşa ediyorsanız
  • Build performansı kritikse
  • Erken benimseyen olma lüksünüz varsa
  • Ekipleriniz Rust tabanlı araçlarda rahatsa

Seçenek 3: Web Workers + SharedArrayBuffer

Yüksek performanslı iletişim için, Web Workers ile SharedArrayBuffer'ı değerlendirdik:

typescript
// SharedArrayBuffer kullanarak yüksek performanslı iletişimclass SharedArrayBridge {  private sharedBuffer: SharedArrayBuffer;  private int32Array: Int32Array;  private messageQueue: ArrayBuffer;
  constructor() {    this.sharedBuffer = new SharedArrayBuffer(1024);    this.int32Array = new Int32Array(this.sharedBuffer);    this.messageQueue = new ArrayBuffer(8192);  }
  async request<T>(action: string, payload: any): Promise<T> {    const messageId = this.generateId();
    // Shared buffer'a yaz    const encoder = new TextEncoder();    const message = JSON.stringify({ id: messageId, action, payload });    const bytes = encoder.encode(message);
    // Shared buffer'a kopyala    const uint8Array = new Uint8Array(this.sharedBuffer);    uint8Array.set(bytes, 0);
    // Native tarafına sinyal ver    Atomics.notify(this.int32Array, 0);
    // Yanıt bekle    return new Promise((resolve, reject) => {      const timeout = setTimeout(() => {        reject(new Error('Request timed out'));      }, 10000);
      // Yanıt için polling yap      const checkResponse = () => {        const responseBytes = new Uint8Array(this.sharedBuffer, 512, 512);        const responseText = new TextDecoder().decode(responseBytes);
        try {          const response = JSON.parse(responseText);          if (response.id === messageId) {            clearTimeout(timeout);            resolve(response.data);          } else {            setTimeout(checkResponse, 10);          }        } catch (error) {          setTimeout(checkResponse, 10);        }      };
      checkResponse();    });  }}

Neden seçmedik:

  • Browser desteği: SharedArrayBuffer spesifik header'lar gerektiriyor
  • Karmaşıklık: Implement etmesi ve debug etmesi çok daha karmaşık
  • Güvenlik: Dikkatli bellek yönetimi gerektiriyor
  • Platform farklılıkları: iOS WebView'da farklı SharedArrayBuffer davranışı

SharedArrayBuffer'ı ne zaman kullanmalı:

  • Aşırı yüksek performanslı iletişime ihtiyacınız varsa
  • Yalnızca modern browser'ları hedefliyorsanız
  • Karmaşıklığı kaldırabiliyorsanız
  • Performans basitlikten daha önemliyse

Seçenek 4: WebSocket Bridge

Real-time iletişim için WebSocket'leri değerlendirdik:

typescript
// WebSocket tabanlı bridgeclass WebSocketBridge {  private ws: WebSocket;  private pendingRequests = new Map<string, {    resolve: (value: any) => void;    reject: (error: any) => void;  }>();
  constructor(url: string) {    this.ws = new WebSocket(url);    this.ws.onmessage = this.handleMessage.bind(this);  }
  async request<T>(action: string, payload: any): Promise<T> {    const id = this.generateId();
    return new Promise((resolve, reject) => {      this.pendingRequests.set(id, { resolve, reject });
      this.ws.send(JSON.stringify({        id,        action,        payload,        timestamp: Date.now()      }));    });  }
  private handleMessage(event: MessageEvent) {    const message = JSON.parse(event.data);    const pending = this.pendingRequests.get(message.id);
    if (pending) {      this.pendingRequests.delete(message.id);
      if (message.success) {        pending.resolve(message.data);      } else {        pending.reject(new Error(message.error));      }    }  }}

Neden seçmedik:

  • Network bağımlılığı: Network bağlantısı gerektiriyor
  • Latency: Ek network hop'u
  • Karmaşıklık: WebSocket lifecycle'ını yönetmek gerekiyor
  • Güvenlik: Ek saldırı yüzeyi

WebSocket bridge'i ne zaman kullanmalı:

  • Real-time iletişime ihtiyacınız varsa
  • Network latency kabul edilebilirse
  • Dağıtık sistem inşa ediyorsanız
  • Bi-directional streaming'e ihtiyacınız varsa

İletişim Zorluğu

WebView iletişimi başta basit görünür. Web tarafında postMessage, native tarafında onMessage var. Neyin yanlış gidebileceği?

Her şey, anlaşılan:

  • Mesajlar string, bu yüzden type safety kaybediyorsunuz
  • Built-in request/response pattern'i yok
  • Delivery garantisi veya acknowledgment yok
  • iOS ve Android arasında farklı davranış
  • Timeout veya retry'ları handle etme yolu yok
  • Büyük payload'larla performans degradasyonu

Sağlam Mesaj Protokolü İnşa Etmek

Birkaç iterasyondan sonra, bu sorunları çözen bir protokol geliştirdik:

typescript
// Native ve web arasında paylaşılan tiplerinterface BridgeMessage<T = unknown> {  id: string;  type: MessageType;  action: string;  payload: T;  timestamp: number;  version: string;}
enum MessageType {  REQUEST = 'REQUEST',  RESPONSE = 'RESPONSE',  EVENT = 'EVENT',  ERROR = 'ERROR'}
interface BridgeResponse<T = unknown> {  id: string;  success: boolean;  data?: T;  error?: {    code: string;    message: string;    details?: unknown;  };}

Native Taraf Implementasyonu

React Native tarafındaki production bridge implementasyonumuz:

typescript
import { WebView } from 'react-native-webview';import { EventEmitter } from 'events';
class NativeWebViewBridge extends EventEmitter {  private webViewRef: React.RefObject<WebView>;  private pendingRequests = new Map<string, {    resolve: (value: any) => void;    reject: (error: any) => void;    timeout: NodeJS.Timeout;  }>();  private messageQueue: BridgeMessage[] = [];  private isReady = false;
  constructor(webViewRef: React.RefObject<WebView>) {    super();    this.webViewRef = webViewRef;  }
  // Request gönder ve yanıt bekle  async request<TRequest, TResponse>(    action: string,    payload: TRequest,    timeoutMs = 10000  ): Promise<TResponse> {    const id = this.generateId();    const message: BridgeMessage<TRequest> = {      id,      type: MessageType.REQUEST,      action,      payload,      timestamp: Date.now(),      version: '1.0'    };
    return new Promise((resolve, reject) => {      // Timeout ayarla      const timeout = setTimeout(() => {        this.pendingRequests.delete(id);        reject(new Error(`Request ${action} timed out after ${timeoutMs}ms`));      }, timeoutMs);
      // Pending request'i sakla      this.pendingRequests.set(id, { resolve, reject, timeout });
      // Mesaj gönder      this.sendMessage(message);    });  }
  // Tek yönlü event gönder  emit(action: string, payload?: unknown): void {    const message: BridgeMessage = {      id: this.generateId(),      type: MessageType.EVENT,      action,      payload,      timestamp: Date.now(),      version: '1.0'    };
    this.sendMessage(message);  }
  private sendMessage(message: BridgeMessage): void {    if (!this.isReady) {      // WebView hazır olana kadar mesajları sıraya al      this.messageQueue.push(message);      return;    }
    const serialized = JSON.stringify(message);
    // Kritik: güvenilirlik için injectJavaScript değil postMessage kullan    this.webViewRef.current?.postMessage(serialized);
    // Debugging için log    if (__DEV__) {      console.log(`[Bridge] Sent: ${message.action}`, message);    }  }
  handleMessage(event: WebViewMessageEvent): void {    try {      const message: BridgeMessage = JSON.parse(event.nativeEvent.data);
      switch (message.type) {        case MessageType.RESPONSE:          this.handleResponse(message as BridgeResponse);          break;
        case MessageType.REQUEST:          this.handleRequest(message);          break;
        case MessageType.EVENT:          this.handleEvent(message);          break;
        case MessageType.ERROR:          this.handleError(message);          break;      }    } catch (error) {      console.error('[Bridge] Failed to parse message:', error);    }  }
  private handleResponse(response: BridgeResponse): void {    const pending = this.pendingRequests.get(response.id);    if (!pending) return;
    clearTimeout(pending.timeout);    this.pendingRequests.delete(response.id);
    if (response.success) {      pending.resolve(response.data);    } else {      pending.reject(new Error(response.error?.message || 'Unknown error'));    }  }
  private handleRequest(message: BridgeMessage): void {    // Native handler'ların işlemesi için event emit et    this.emit(`request:${message.action}`, message);  }
  private handleEvent(message: BridgeMessage): void {    this.emit(`event:${message.action}`, message.payload);  }
  private generateId(): string {    return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;  }
  // WebView hazır olduğunu bildirdiğinde çağrılır  onBridgeReady(): void {    this.isReady = true;
    // Sıradaki mesajları gönder    while (this.messageQueue.length > 0) {      const message = this.messageQueue.shift();      if (message) this.sendMessage(message);    }  }}

Web Taraf Implementasyonu

Web tarafının mesajları handle etmesi ve benzer API sağlaması gerekiyor:

typescript
// WebViewBridge.ts - WebView'a inject edilenclass WebViewBridge {  private handlers = new Map<string, (payload: any) => Promise<any>>();  private eventListeners = new Map<string, Set<(payload: any) => void>>();
  constructor() {    // Native'den gelen mesajları dinle    window.addEventListener('message', this.handleMessage.bind(this));
    // Android için window.ReactNativeWebView'ı override et    if (!window.ReactNativeWebView) {      window.ReactNativeWebView = {        postMessage: (message: string) => {          window.postMessage(message, '*');        }      };    }
    // Bridge hazır sinyali ver    this.emit('BRIDGE_READY');  }
  // Request handler kaydet  handle<TRequest, TResponse>(    action: string,    handler: (payload: TRequest) => Promise<TResponse>  ): void {    this.handlers.set(action, handler);  }
  // Native'e request gönder  async request<TRequest, TResponse>(    action: string,    payload: TRequest  ): Promise<TResponse> {    const id = this.generateId();    const message: BridgeMessage<TRequest> = {      id,      type: MessageType.REQUEST,      action,      payload,      timestamp: Date.now(),      version: '1.0'    };
    return new Promise((resolve, reject) => {      const timeout = setTimeout(() => {        reject(new Error(`Request ${action} timed out`));      }, 10000);
      const responseHandler = (event: MessageEvent) => {        try {          const response: BridgeMessage = JSON.parse(event.data);
          if (response.type === MessageType.RESPONSE && response.id === id) {            clearTimeout(timeout);            window.removeEventListener('message', responseHandler);
            if ((response as BridgeResponse).success) {              resolve((response as BridgeResponse).data);            } else {              reject(new Error((response as BridgeResponse).error?.message));            }          }        } catch (error) {          // Diğer mesajlardan gelen parsing hatalarını yoksay        }      };
      window.addEventListener('message', responseHandler);      this.sendMessage(message);    });  }
  // Native'e event gönder  emit(action: string, payload?: unknown): void {    const message: BridgeMessage = {      id: this.generateId(),      type: MessageType.EVENT,      action,      payload,      timestamp: Date.now(),      version: '1.0'    };
    this.sendMessage(message);  }
  // Native'den gelen event'leri dinle  on(action: string, listener: (payload: any) => void): () => void {    if (!this.eventListeners.has(action)) {      this.eventListeners.set(action, new Set());    }
    this.eventListeners.get(action)!.add(listener);
    // Unsubscribe fonksiyonu döndür    return () => {      this.eventListeners.get(action)?.delete(listener);    };  }
  private async handleMessage(event: MessageEvent): Promise<void> {    try {      const message: BridgeMessage = JSON.parse(event.data);
      if (message.type === MessageType.REQUEST) {        await this.handleRequest(message);      } else if (message.type === MessageType.EVENT) {        this.handleEvent(message);      }    } catch (error) {      // Bridge olmayan mesajları yoksay    }  }
  private async handleRequest(message: BridgeMessage): Promise<void> {    const handler = this.handlers.get(message.action);
    if (!handler) {      this.sendResponse(message.id, false, null, {        code: 'HANDLER_NOT_FOUND',        message: `No handler registered for action: ${message.action}`      });      return;    }
    try {      const result = await handler(message.payload);      this.sendResponse(message.id, true, result);    } catch (error) {      this.sendResponse(message.id, false, null, {        code: 'HANDLER_ERROR',        message: error instanceof Error ? error.message : 'Unknown error',        details: error      });    }  }
  private handleEvent(message: BridgeMessage): void {    const listeners = this.eventListeners.get(message.action);    if (listeners) {      listeners.forEach(listener => {        try {          listener(message.payload);        } catch (error) {          console.error(`Event listener error for ${message.action}:`, error);        }      });    }  }
  private sendMessage(message: BridgeMessage): void {    const serialized = JSON.stringify(message);
    if (window.ReactNativeWebView?.postMessage) {      window.ReactNativeWebView.postMessage(serialized);    } else {      window.parent.postMessage(serialized, '*');    }  }
  private sendResponse(    id: string,    success: boolean,    data?: unknown,    error?: any  ): void {    const response: BridgeResponse = {      id,      success,      data,      error    };
    this.sendMessage({      ...response,      type: MessageType.RESPONSE,      action: 'response',      payload: data,      timestamp: Date.now(),      version: '1.0'    });  }
  private generateId(): string {    return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;  }}
// Bridge'i global olarak initialize etdeclare global {  interface Window {    bridge: WebViewBridge;    ReactNativeWebView?: {      postMessage: (message: string) => void;    };  }}
window.bridge = new WebViewBridge();

Type-Safe İletişim

En büyük kazanımlarımızdan biri bridge üzerinde type safety elde etmekti. İşte nasıl yaptığımız:

typescript
// Paylaşılan tipler dosyası (hem native hem web tarafından kullanılır)export interface BridgeAPI {  // Authentication  'auth.getToken': {    request: void;    response: { token: string; expiresAt: number };  };
  'auth.refreshToken': {    request: { currentToken: string };    response: { token: string; expiresAt: number };  };
  // Navigation  'navigation.navigate': {    request: { screen: string; params?: Record<string, any> };    response: void;  };
  'navigation.goBack': {    request: void;    response: boolean;  };
  // Native özellikler  'camera.takePhoto': {    request: {      quality?: number;      allowEdit?: boolean;    };    response: {      uri: string;      width: number;      height: number;    };  };
  'biometrics.authenticate': {    request: { reason: string };    response: { success: boolean };  };
  // Analytics  'analytics.track': {    request: {      event: string;      properties?: Record<string, any>;    };    response: void;  };}
// Type-safe bridge wrapperexport class TypedBridge {  constructor(private bridge: WebViewBridge | NativeWebViewBridge) {}
  async request<K extends keyof BridgeAPI>(    action: K,    payload: BridgeAPI[K]['request']  ): Promise<BridgeAPI[K]['response']> {    return this.bridge.request(action, payload);  }
  on<K extends keyof BridgeAPI>(    action: K,    handler: (payload: BridgeAPI[K]['request']) => void  ): () => void {    return this.bridge.on(action, handler);  }}

Kullanım tamamen type-safe hale geliyor:

typescript
// WebView içindeconst bridge = new TypedBridge(window.bridge);
// TypeScript tam request/response tiplerini biliyorconst { token } = await bridge.request('auth.getToken', undefined);const photo = await bridge.request('camera.takePhoto', { quality: 0.8 });
// Yanlış parametre geçirirseniz type error// bridge.request('auth.getToken', { wrong: 'param' }); // Bad: TypeScript error

Servis Entegrasyon Pattern'leri

Bridge üzerinden çeşitli servisleri nasıl entegre ettiğimiz:

Authentication Service

typescript
// Native tarafclass AuthBridgeHandler {  constructor(    private bridge: NativeWebViewBridge,    private authService: AuthService  ) {    this.setupHandlers();  }
  private setupHandlers(): void {    this.bridge.on('request:auth.getToken', async (message) => {      try {        const token = await this.authService.getAccessToken();
        if (!token) {          throw new Error('No active session');        }
        this.bridge.sendResponse(message.id, true, {          token,          expiresAt: this.authService.getTokenExpiry()        });      } catch (error) {        this.bridge.sendResponse(message.id, false, null, {          code: 'AUTH_ERROR',          message: error.message        });      }    });
    this.bridge.on('request:auth.refreshToken', async (message) => {      try {        const newToken = await this.authService.refreshToken();
        this.bridge.sendResponse(message.id, true, {          token: newToken,          expiresAt: this.authService.getTokenExpiry()        });      } catch (error) {        // Refresh başarısız olursa, yeniden login'e zorla        this.bridge.emit('auth.sessionExpired');
        this.bridge.sendResponse(message.id, false, null, {          code: 'REFRESH_FAILED',          message: 'Session expired'        });      }    });  }}
// Web taraf - Otomatik yenilenen fetch wrapperclass AuthenticatedFetch {  constructor(private bridge: TypedBridge) {}
  async fetch(url: string, options: RequestInit = {}): Promise<Response> {    let token = await this.getValidToken();
    const response = await fetch(url, {      ...options,      headers: {        ...options.headers,        'Authorization': `Bearer ${token}`      }    });
    // Unauthorized ise yenilenmiş token ile tekrar dene    if (response.status === 401) {      token = await this.refreshToken();
      return fetch(url, {        ...options,        headers: {          ...options.headers,          'Authorization': `Bearer ${token}`        }      });    }
    return response;  }
  private async getValidToken(): Promise<string> {    const cached = this.getCachedToken();
    if (cached && cached.expiresAt > Date.now() + 60000) {      return cached.token;    }
    return this.refreshToken();  }
  private async refreshToken(): Promise<string> {    const { token, expiresAt } = await this.bridge.request(      'auth.refreshToken',      { currentToken: this.getCachedToken()?.token }    );
    this.cacheToken(token, expiresAt);    return token;  }
  private cacheToken(token: string, expiresAt: number): void {    sessionStorage.setItem('bridge_token', JSON.stringify({      token,      expiresAt    }));  }
  private getCachedToken(): { token: string; expiresAt: number } | null {    const cached = sessionStorage.getItem('bridge_token');    return cached ? JSON.parse(cached) : null;  }}

Native Özellik Erişimi

Native özellikleri WebView'lara nasıl açtığımız:

typescript
// Kamera entegrasyonuclass CameraBridgeHandler {  constructor(    private bridge: NativeWebViewBridge,    private imagePicker: ImagePicker  ) {    this.bridge.on('request:camera.takePhoto', async (message) => {      try {        const { quality = 0.8, allowEdit = false } = message.payload || {};
        const result = await this.imagePicker.launchCamera({          mediaType: 'photo',          quality,          allowsEditing: allowEdit,          // Önemli: WebView uyumluluğu için base64          includeBase64: true        });
        if (result.didCancel) {          throw new Error('User cancelled');        }
        if (result.errorMessage) {          throw new Error(result.errorMessage);        }
        const asset = result.assets?.[0];        if (!asset) {          throw new Error('No image selected');        }
        // WebView için data URI'ye dönüştür        const dataUri = `data:${asset.type};base64,${asset.base64}`;
        this.bridge.sendResponse(message.id, true, {          uri: dataUri,          width: asset.width!,          height: asset.height!        });      } catch (error) {        this.bridge.sendResponse(message.id, false, null, {          code: 'CAMERA_ERROR',          message: error.message        });      }    });  }}
// Web taraf kullanımıasync function uploadProfilePhoto() {  try {    const photo = await bridge.request('camera.takePhoto', {      quality: 0.9,      allowEdit: true    });
    // Data URI'yi upload için blob'a dönüştür    const blob = await dataURItoBlob(photo.uri);
    // Authenticated fetch kullanarak upload    const formData = new FormData();    formData.append('photo', blob);
    const response = await authenticatedFetch.fetch('/api/profile/photo', {      method: 'POST',      body: formData    });
    return response.json();  } catch (error) {    if (error.message === 'User cancelled') {      // İptal durumunu handle et      return null;    }    throw error;  }}

Performans Optimizasyonu

Production'a deploy ettikten sonra, birkaç performans darboğazı keşfettik:

Büyük Payload Handling

PostMessage üzerinden büyük payload'lar (resimler, dökümanlar) göndermek yavaştı ve UI'yı dondurabıliyordu. Çözümümüz chunking'di:

typescript
class ChunkedMessageHandler {  private chunks = new Map<string, {    chunks: string[];    receivedCount: number;    totalChunks: number;  }>();
  // Büyük mesajları chunk'lara böl  sendChunked(message: BridgeMessage, chunkSize = 50000): void {    const serialized = JSON.stringify(message);
    if (serialized.length <= chunkSize) {      // Doğrudan göndermek için yeterince küçük      this.send(serialized);      return;    }
    // Chunk'lara böl    const chunks: string[] = [];    for (let i = 0; i < serialized.length; i += chunkSize) {      chunks.push(serialized.slice(i, i + chunkSize));    }
    const chunkId = this.generateId();
    // Her chunk'ı gönder    chunks.forEach((chunk, index) => {      this.send(JSON.stringify({        type: 'CHUNK',        chunkId,        chunkIndex: index,        totalChunks: chunks.length,        data: chunk      }));    });  }
  handleChunk(message: any): BridgeMessage | null {    const { chunkId, chunkIndex, totalChunks, data } = message;
    if (!this.chunks.has(chunkId)) {      this.chunks.set(chunkId, {        chunks: new Array(totalChunks),        receivedCount: 0,        totalChunks      });    }
    const chunkData = this.chunks.get(chunkId)!;    chunkData.chunks[chunkIndex] = data;    chunkData.receivedCount++;
    // Tüm chunk'lar alındı mı kontrol et    if (chunkData.receivedCount === chunkData.totalChunks) {      const complete = chunkData.chunks.join('');      this.chunks.delete(chunkId);
      try {        return JSON.parse(complete);      } catch (error) {        console.error('Failed to parse chunked message:', error);        return null;      }    }
    return null;  }}

Mesaj Batching

Analytics gibi yüksek frekanslı event'ler için batching implement ettik:

typescript
class BatchedBridge extends NativeWebViewBridge {  private batch: BridgeMessage[] = [];  private batchTimeout?: NodeJS.Timeout;  private batchSize = 10;  private batchDelay = 100; // ms
  emit(action: string, payload?: unknown): void {    if (this.shouldBatch(action)) {      this.addToBatch({        id: this.generateId(),        type: MessageType.EVENT,        action,        payload,        timestamp: Date.now(),        version: '1.0'      });    } else {      super.emit(action, payload);    }  }
  private shouldBatch(action: string): boolean {    // Analytics ve kritik olmayan event'leri batch'le    return action.startsWith('analytics.') ||           action.startsWith('metrics.');  }
  private addToBatch(message: BridgeMessage): void {    this.batch.push(message);
    if (this.batch.length >= this.batchSize) {      this.flushBatch();    } else if (!this.batchTimeout) {      this.batchTimeout = setTimeout(() => {        this.flushBatch();      }, this.batchDelay);    }  }
  private flushBatch(): void {    if (this.batch.length === 0) return;
    const batchMessage: BridgeMessage = {      id: this.generateId(),      type: MessageType.EVENT,      action: 'batch',      payload: this.batch,      timestamp: Date.now(),      version: '1.0'    };
    super.sendMessage(batchMessage);
    this.batch = [];    if (this.batchTimeout) {      clearTimeout(this.batchTimeout);      this.batchTimeout = undefined;    }  }}

Debugging ve İzleme

Production debugging başlangıçta kâbustu. İşte nasıl yönetilebilir hale getirdik:

Mesaj Logging ve Replay

typescript
class BridgeDebugger {  private messageLog: Array<{    timestamp: number;    direction: 'sent' | 'received';    message: BridgeMessage;    duration?: number;  }> = [];
  private maxLogSize = 1000;
  logSent(message: BridgeMessage): void {    this.addToLog('sent', message);  }
  logReceived(message: BridgeMessage, duration?: number): void {    this.addToLog('received', message, duration);  }
  private addToLog(    direction: 'sent' | 'received',    message: BridgeMessage,    duration?: number  ): void {    this.messageLog.push({      timestamp: Date.now(),      direction,      message,      duration    });
    // Log boyutunu yönetilebilir tut    if (this.messageLog.length > this.maxLogSize) {      this.messageLog.shift();    }  }
  // Debugging için logları export et  exportLogs(): string {    return JSON.stringify(this.messageLog, null, 2);  }
  // Performans metriklerini al  getMetrics(): {    totalMessages: number;    averageResponseTime: number;    slowestActions: Array<{ action: string; duration: number }>;    errorRate: number;  } {    const requests = this.messageLog.filter(      log => log.message.type === MessageType.REQUEST    );
    const responses = this.messageLog.filter(      log => log.message.type === MessageType.RESPONSE && log.duration    );
    const errors = this.messageLog.filter(      log => log.message.type === MessageType.ERROR    );
    const responseTimes = responses      .map(r => r.duration!)      .filter(d => d > 0);
    const avgResponseTime = responseTimes.length > 0      ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length      : 0;
    const slowest = responses      .filter(r => r.duration)      .sort((a, b) => b.duration! - a.duration!)      .slice(0, 10)      .map(r => ({        action: r.message.action,        duration: r.duration!      }));
    return {      totalMessages: this.messageLog.length,      averageResponseTime: Math.round(avgResponseTime),      slowestActions: slowest,      errorRate: errors.length / requests.length    };  }}

Remote Debugging Kurulumu

Production debugging için remote debugging yeteneği implement ettik:

typescript
// Yalnızca development - remote debuggingif (__DEV__) {  const enableRemoteDebugging = () => {    const ws = new WebSocket('ws://localhost:8080/bridge-debug');
    ws.onopen = () => {      console.log('[Bridge Debug] Connected to debugger');    };
    // Tüm bridge mesajlarını debugger'a forward et    bridge.on('*', (message) => {      ws.send(JSON.stringify({        type: 'BRIDGE_MESSAGE',        timestamp: Date.now(),        message      }));    });  };}

Gerçek Dünya Zorlukları ve Çözümleri

Race Condition Bug'ı

Bahsettiğim ödeme akışı bug'ını hatırlıyor musunuz? İşte neler oluyordu:

  1. WebView auth token talep ediyor
  2. Native app token refresh başlatıyor
  3. WebView yanıt beklerken timeout oluyor
  4. Native app refresh'i tamamlıyor ve yanıt gönderiyor
  5. WebView çoktan devam etmiş, yanıt yoksayılıyor

Düzeltme, uygun request cancellation implement etmeyi gerektirdi:

typescript
class CancellableRequest {  private cancelled = false;  private cleanupFns: Array<() => void> = [];
  constructor(    private promise: Promise<any>,    private onCancel?: () => void  ) {}
  then(onFulfilled: any, onRejected: any): Promise<any> {    return this.promise.then(      (value) => {        if (this.cancelled) {          throw new Error('Request cancelled');        }        return onFulfilled(value);      },      onRejected    );  }
  cancel(): void {    this.cancelled = true;    this.onCancel?.();    this.cleanupFns.forEach(fn => fn());  }
  addCleanup(fn: () => void): void {    this.cleanupFns.push(fn);  }}
// Bridge'de kullanımrequest<TRequest, TResponse>(  action: string,  payload: TRequest,  timeoutMs = 10000): CancellableRequest {  const id = this.generateId();  let timeoutId: NodeJS.Timeout;
  const promise = new Promise((resolve, reject) => {    timeoutId = setTimeout(() => {      this.pendingRequests.delete(id);      reject(new Error(`Request ${action} timed out`));    }, timeoutMs);
    this.pendingRequests.set(id, { resolve, reject, timeout: timeoutId });    this.sendMessage(message);  });
  const request = new CancellableRequest(promise, () => {    clearTimeout(timeoutId);    this.pendingRequests.delete(id);
    // Native tarafına cancellation'ı bildir    this.emit('request.cancelled', { requestId: id });  });
  request.addCleanup(() => clearTimeout(timeoutId));
  return request;}

Platform-Spesifik Quirk'ler

iOS ve Android WebView'lar ince yollarla farklı davranıyor:

typescript
// Platform-spesifik mesaj handlingclass PlatformBridge extends NativeWebViewBridge {  sendMessage(message: BridgeMessage): void {    if (Platform.OS === 'ios') {      // iOS postMessage için spesifik timing gerektiriyor      requestAnimationFrame(() => {        super.sendMessage(message);      });    } else {      // Android hemen gönderebiliyor      super.sendMessage(message);    }  }
  handleMessage(event: WebViewMessageEvent): void {    // Android data'yı string olarak gönderiyor, iOS bazen object    const data = typeof event.nativeEvent.data === 'string'      ? event.nativeEvent.data      : JSON.stringify(event.nativeEvent.data);
    try {      const message = JSON.parse(data);      super.handleMessage({        nativeEvent: { ...event.nativeEvent, data }      });    } catch (error) {      console.error('[Bridge] Platform parsing error:', error);    }  }}

Performans Sonuçları

Bu pattern'ler metriklerimizde önemli iyileşmelere yol açtı:

Mesaj Performansı

  • Ortalama yanıt süresi: 45ms (180ms'den düştü)
    1. yüzdelik: 200ms (2s'den düştü)
  • Başarısız mesajlar: 0.01% (2.3%'ten düştü)

Bellek Kullanımı

  • Bridge overhead'i: WebView başına ~5MB
  • Mesaj kuyruğu zirvesi: 15MB (batching ile)
  • 24 saatlik stress test sonrası memory leak yok

Batarya Etkisi

  • 5% batarya tüketimi azalması
  • Esas olarak batching ve mesaj frekansını azaltmaktan

Önemli Çıkarımlar

  1. Başarısızlık için Tasarla: Her mesaj başarısız olabilir. İlk günden retry ve timeout handling inşa et.

  2. Type Safety Kritik: Bridge üzerinde TypeScript tiplerindeki yatırım, debugging süresini 10x azaltarak geri döndü.

  3. Performans Batching Gerektiriyor: Bireysel mesajlar pahalı. Mümkün olduğunda batch'le.

  4. Platform Farklılıkları Önemli: Hem iOS hem Android'de, özellikle eski cihazlarda iyice test et.

  5. Debugging Araçları Hayati: Göremediğini düzeltemezsin. Kapsamlı logging'i erken inşa et.

  6. Alternatif Yaklaşımların Trade-off'ları Var: Her iletişim metodunun güçlü yanları var. Spesifik ihtiyaçlarınıza göre seçin.

Sırada Ne Var?

3. Bölüm'te şunları keşfedeceğiz:

  • Multi-channel rendering (aynı micro frontend app, web ve desktop'ta)
  • Production performans optimizasyon teknikleri
  • Offline mod ve sync handling
  • Güvenlik değerlendirmeleri ve sandboxing

İletişim katmanı mobil micro frontend'lerin kalbidir. Doğru yaparsanız, diğer her şey yönetilebilir hale gelir. Yanlış yaparsanız, bizim gibi sabah 3'te race condition'ları debug edersiniz.

Bir dahaki sefere, bu mimarinin birden fazla platformda nasıl ölçeklendiğini ve production'da bulduğumuz şaşırtıcı optimizasyonları inceleyeceğiz.

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.

İlerleme2/3 yazı tamamlandı

İlgili Yazılar