İçeriğe atla

React Native WebView İletişimi: postMessage Bridge Pattern'leri

WebView-native iletişim pattern'leri: mesaj geçişi, servis entegrasyonu ve çalışan kodla tipli bir request-response bridge'i.

Ayhan Sipahi Ayhan Sipahi

Mobil micro frontend’lerde WebView-native iletişimi üç somut biçimde bozulur: mesajlar sırasız gelir, TypeScript tipleri web ve native tarafta ayrışır, WebView içindeki JavaScript istisnaları native crash reporter’larda stack trace bırakmaz. Yapılandırılmış bir bridge katmanı yoksa bu hatalar birbirini besler; mesaj geçişindeki bir race condition, ödeme onaylarını veya auth token’larını sessizce yutabilir.

Varsayılan olarak seçilmesi gereken yapı, tipli bir request-response bridge’i: her mesaj bir id taşır, her istek bir timeout taşır ve iki taraf da tek bir TypeScript sözleşmesine göre derlenir. Açık genellikle native kimlik doğrulama ile WebView checkout arasında bölünmüş ödeme akışında görünür; çünkü race yalnızca JavaScript motoru yavaş olan cihazlarda ortaya çıkar. Protokol tanımından sonraki her şey (servis entegrasyonu, chunking, batching, platform farkları) bu tek sözleşmeye bağlanır.

Mobil Micro Frontend Serisi#

Bu, mobil micro frontend serisinin 2. bölümü:

1. Bölümü okumadınız mı? Mimari temeller için oradan başlayın.

Production’a hazır mısınız? Optimizasyon stratejileri için 3. Bölüm’e geçin.

İletişimin Zor Yanı#

WebView iletişimi başta basit görünür. Web tarafında postMessage, native tarafında onMessage var. Ne ters gidebilir ki?

Anlaşılan her şey:

  • Mesajlar string, bu yüzden type safety kayboluyor
  • 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 düşüşü

Alternatif İletişim Yaklaşımları#

Tipli bir postMessage bridge’ine dört yaklaşım rakip. Her biri yukarıdaki listenin bir kısmını çözüyor, her biri bedelini başka bir yerden alıyor.

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

Re.Pack (yeni sekmede açılır), 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.

Kurulum:

// 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.ts
export interface NativeBridge {
  getAuthToken(): Promise<string>;
  openCamera(): Promise<string>;
  navigate(screen: string, params?: any): void;
}

// Micro frontend içinde
import { 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>;
};

Bedeli:

  • Karmaşıklık: Tüm ekiplerin Module Federation’a aynı anda geçmesi gerekiyor
  • Type safety: Paylaşılan modüllerin ayrı bir package’da yaşaması gerekiyor
  • Versioning: Modül versiyon çakışmalarını debug etmek zor
  • Performans: İlk bundle boyutu belirgin şekilde büyüyor
  • Debugging: Stack trace’ler birden fazla context’e yayılıyor

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#

Rspack’in Module Federation’ı iletişim için başka bir seçenektir:

// rspack.config.mjs
export 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.ts
interface BridgeTransport {
  send(action: string, payload?: unknown): Promise<unknown>;
}

export class RspackBridge {
  private static instance: RspackBridge;

  // Module Federation modülü paylaşır, kanalı değil. Transport'u
  // (postMessage, WebSocket, custom scheme) host'un vermesi gerekir.
  private transport?: BridgeTransport;

  static getInstance(): RspackBridge {
    if (!RspackBridge.instance) {
      RspackBridge.instance = new RspackBridge();
    }
    return RspackBridge.instance;
  }

  setTransport(transport: BridgeTransport): void {
    this.transport = transport;
  }

  async request<T>(action: string, payload?: unknown): Promise<T> {
    const transport = this.transport;
    if (!transport) {
      throw new Error('RspackBridge: no transport configured');
    }

    return new Promise<T>((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error(`Request ${action} timed out`));
      }, 10000);

      transport.send(action, payload)
        .then((result) => {
          clearTimeout(timeout);
          resolve(result as T);
        })
        .catch((error: unknown) => {
          clearTimeout(timeout);
          reject(error);
        });
    });
  }
}

Bedeli:

  • React Native uyumluluğu: React Native desteği sınırlı
  • Ekosistem olgunluğu: Debugging araçları ve örnekler hâlâ zayıf
  • Ekip adaptasyonu: webpack’e alışkın ekipler için ciddi bir yeniden eğitim
  • Production kararlılığı: webpack tabanlı çözümlerden daha yeni

Rspack Module Federation’ı ne zaman kullanmalı:

  • Yalnızca web micro frontend’leri inşa ediyorsanız
  • Build performansı kritikse
  • Hızla değişen bir ekosistemle çalışabiliyorsanız
  • Ekipleriniz Rust tabanlı araçlarda rahatsa

Seçenek 3: Web Workers + SharedArrayBuffer#

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

// SharedArrayBuffer kullanarak yüksek performanslı iletişim
class 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();
    });
  }
}

Bedeli:

  • Browser desteği: SharedArrayBuffer, COOP ve COEP 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’ler bir aday:

// WebSocket tabanlı bridge
class 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));
      }
    }
  }
}

Bedeli:

  • Network bağımlılığı: Network bağlantısı gerektiriyor
  • Latency: Ek network hop’u
  • Karmaşıklık: WebSocket lifecycle’ının yönetilmesi 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

Mesaj Protokolü Tasarımı#

Birkaç iterasyonun ardından şekillenen aşağıdaki protokol bu sorunları çözer:

// Native ve web arasında paylaşılan tipler
interface 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ın Uygulanması#

React Native tarafında production bridge implementasyonu:

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

  // WebView'a tek yönlü event gönder. EventEmitter'ın kendi emit()'i
  // native handler'lara yerel dağıtım için serbest kalır.
  sendEvent(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);
  }

  // WebView'dan gelen bir request'i yanıtla
  sendResponse(
    id: string,
    success: boolean,
    data?: unknown,
    error?: BridgeResponse['error']
  ): void {
    this.sendMessage({
      id,
      success,
      data,
      error,
      type: MessageType.RESPONSE,
      action: 'response',
      payload: data,
      timestamp: Date.now(),
      version: '1.0'
    } as BridgeMessage & BridgeResponse);
  }

  protected 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);

    // postMessage, WebView'ın window 'message' dinleyicisine düşer
    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 handleError(message: BridgeMessage): void {
    // 'error' değil: EventEmitter dinlenmeyen 'error' event'inde hata fırlatır
    this.emit('bridge:error', message);
  }

  protected 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ının Uygulanması#

Web tarafının mesajları handle etmesi ve benzer bir API sunması gerekiyor:

// WebViewBridge.ts - WebView'a inject edilen
class 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 et
declare global {
  interface Window {
    bridge: WebViewBridge;
    ReactNativeWebView?: {
      postMessage: (message: string) => void;
    };
  }
}

window.bridge = new WebViewBridge();

Type-Safe İletişim#

Bridge üzerinde type safety, her iki taraftan kullanılan ortak bir tipler dosyasıyla elde edilir:

// 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 wrapper
export 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:

// WebView içinde
const bridge = new TypedBridge(window.bridge);

// TypeScript tam request/response tiplerini biliyor
const { token } = await bridge.request('auth.getToken', undefined);
const photo = await bridge.request('camera.takePhoto', { quality: 0.8 });

// Yanlış payload şekli derleme anında patlar
// bridge.request('auth.getToken', { wrong: 'param' }); // TypeScript error

Servis Entegrasyon Pattern’leri#

Çeşitli servisler bridge üzerinden aşağıdaki pattern’lerle entegre edilir:

Authentication Service#

// Native taraf
class 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.sendEvent('auth.sessionExpired');

        this.bridge.sendResponse(message.id, false, null, {
          code: 'REFRESH_FAILED',
          message: 'Session expired'
        });
      }
    });
  }
}

// Web taraf - Otomatik yenilenen fetch wrapper
class 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 özellikler WebView’lara şu pattern’le açılır:

// Kamera entegrasyonu
class 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#

Hemen her bridge implementasyonunda iki darboğaz ortaya çıkar:

Büyük Payload Yönetimi#

PostMessage üzerinden büyük payload’lar (resimler, dökümanlar) göndermek yavaştır ve UI’yı dondurabilir. Çözüm chunking’dir:

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’lerin kendi round trip’ine ihtiyacı yok. Batching onları kritik yoldan çıkarır:

class BatchedBridge extends NativeWebViewBridge {
  private batch: BridgeMessage[] = [];
  private batchTimeout?: NodeJS.Timeout;
  private batchSize = 10;
  private batchDelay = 100; // ms

  sendEvent(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.sendEvent(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’da debugging başlangıçta zordur. Aşağıdaki araçlar bunu yönetilebilir kılar:

Mesaj Logging ve Replay#

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#

Bir remote debugging kancası, geliştirme sırasında mesaj akışını görünür tutar:

// Yalnızca development - remote debugging
if (__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
      }));
    });
  };
}

Sahada Karşılaşılan Sorunlar ve Çözümleri#

Race Condition Bug’ı#

Açıklanan ödeme akışı race condition şu adımlarla ilerler:

  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:

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ım
request<TRequest>(
  action: string,
  payload: TRequest,
  timeoutMs = 10000
): CancellableRequest {
  const id = this.generateId();
  let timeoutId: NodeJS.Timeout;

  const message: BridgeMessage<TRequest> = {
    id,
    type: MessageType.REQUEST,
    action,
    payload,
    timestamp: Date.now(),
    version: '1.0'
  };

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

    // Karşı tarafa cancellation'ı bildir
    this.sendEvent('request.cancelled', { requestId: id });
  });

  request.addCleanup(() => clearTimeout(timeoutId));

  return request;
}

Platforma Özgü Davranış Farkları#

iOS ve Android WebView’ları ince ayrıntılarda farklı davranıyor:

// Platform-spesifik mesaj handling
class PlatformBridge extends NativeWebViewBridge {
  protected 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 {
      JSON.parse(data); // devretmeden önce doğrula
      super.handleMessage({
        ...event,
        nativeEvent: { ...event.nativeEvent, data }
      } as WebViewMessageEvent);
    } catch (error) {
      console.error('[Bridge] Platform parsing error:', error);
    }
  }
}

Tipli Bridge Ne Zaman Varsayılan Olur#

Tipli bir request-response bridge’i, WebView native yeteneklere geri çağrı yapıyorsa ve iki taraf farklı release döngülerinde yayınlanıyorsa maliyetini çıkarır. Yavaş bir token refresh’inin ödeme onayını sessizce düşürmesini engelleyen şey, id ve timeout taşıyan zarftır. İki durumda tersi geçerli. WebView yalnızca içerik gösteren bir yaprak ekransa, tek yönlü bir injectedJavaScript payload’ı daha basit ve bakımı daha ucuzdur. Tüm modüller tek bir pipeline’dan derlenip dağıtılıyorsa Module Federation, versiyonlanacak bir wire protokolü olmadan doğrudan fonksiyon çağrısı verir.

Sonraki Bölüm#

3. Bölüm şunları kapsıyor:

  • 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

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

Bu serideki tüm yazılar

İlgili yazılar