2025-09-04
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.
WebView-native iletişimini doğru kurmak, mobil micro frontend mimarisinin en zorlu parçalarından biridir. Mesajlar kaybolur, platformlar arası tipler eşleşmez, production’da WebView içlerini izlemek güçtür.
Yaygın bir hata senaryosu: native kimlik doğrulama ve WebView checkout arasında bölünmüş ödeme akışı, yalnızca daha yavaş JavaScript motorlarına sahip cihazlarda ortaya çıkan bir race condition nedeniyle Android kullanıcılarının 15%‘inde başarısız olur. WebView hazır olmadan gönderilen mesajlar sessizce kaybolur.
Bu yazı, iletişim katmanını sıfırdan nasıl inşa edeceğinizi ve en yaygın production WebView iletişim sorunlarını çözen pattern’leri ele alır. TypeScript ile type-safe bridge’ler ve ready-state polling bu sorunları giderir; aşağıda uygulanabilir kod örnekleri yer almaktadır.
Mobil Micro Frontend Serisi
Bu, mobil micro frontend serisinin 2. bölümü. PostMessage, bridge pattern’leri ve production deneyimleriyle devam eder:
- 1. Bölüm: Mimari temeller ve WebView entegrasyon pattern’leri
- 2. Bölüm (Buradasınız): WebView iletişim pattern’leri ve servis entegrasyonu
- 3. Bölüm: Multi-channel mimari ve production optimizasyonu
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üne dalmadan önce, değerlendirilen alternatif yaklaşımlar ve her birinin trade-off’ları aşağıda özetlenmiştir. PostMessage, Re.Pack Module Federation ve custom URL scheme’leri farklı avantaj-dezavantaj dengeleri sunar.
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.
Deneme kurulumu:
// 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>;
};
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
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
export 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ğ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();
});
}
}
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’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));
}
}
}
}
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ç 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 Implementasyonu
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);
});
}
// 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:
// 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ış parametre geçirirseniz type error
// bridge.request('auth.getToken', { wrong: 'param' }); // Bad: 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.emit('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
Production deployment’larında tutarlı olarak birkaç performans darboğazı görülür:
Büyük Payload Handling
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’ler için batching uygulanır:
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. Aşağıdaki araçlar onu 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
Production debugging için remote debugging yeteneği şöyle implement edilir:
// 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
}));
});
};
}
Gerçek Dünya Zorlukları ve Çözümleri
Race Condition Bug’ı
Açıklanan ödeme akışı race condition şu adımlarla ilerler:
- WebView auth token talep ediyor
- Native app token refresh başlatıyor
- WebView yanıt beklerken timeout oluyor
- Native app refresh’i tamamlıyor ve yanıt gönderiyor
- 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, 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:
// Platform-spesifik mesaj handling
class 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 production metriklerinde önemli iyileşmeler sağlar:
Mesaj Performansı
- Ortalama yanıt süresi: 45ms (180ms’den düştü)
-
- 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
-
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.
-
Type Safety Kritik: Bridge üzerinde TypeScript tiplerindeki yatırım, debugging süresini 10x azaltarak geri döndü.
-
Performans Batching Gerektiriyor: Bireysel mesajlar pahalı. Mümkün olduğunda batch’le.
-
Platform Farklılıkları Önemli: Hem iOS hem Android’de, özellikle eski cihazlarda iyice test et.
-
Debugging Araçları Hayati: Göremediğini düzeltemezsin. Kapsamlı logging’i erken inşa et.
-
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 kurulduğunda diğer her şey yönetilebilir hale gelir. Yanlış kurulduğunda race condition’lar kritik anlarda yüzeye çıkar.
- Bölüm, bu mimarinin birden fazla platformda nasıl ölçeklendiğini ve production’da en çok değer katan optimizasyon tekniklerini ele alır.
Kaynaklar
- react-native-webview Rehberi - İletişim - postMessage ve onMessage üzerinden JavaScript-native ve native-JavaScript mesaj geçişini kapsayan resmi rehber
- react-native-webview API Referansı - injectedJavaScript, onMessage ve postMessage dahil WebView prop’ları için tam API referansı
- Window: postMessage() - MDN Web Docs - WebView ile native kod arasındaki çapraz kaynak iletişiminde kullanılan postMessage API’si için MDN referansı
- Re.Pack - Module Federation İletişimi - React Native mikro frontend’lerinde modüller arası iletişim kalıpları üzerine Re.Pack dokümantasyonu
- React Native Performans Genel Bakış - Bridge mesaj işleme ile ilgili thread modeli ve performans değerlendirmeleri üzerine resmi dokümantasyon
- Expo WebView Dokümantasyonu - İletişim kurulumu ve platforma özgü davranış dahil WebView yapılandırması için Expo referansı
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.
Serideki tüm yazılar
İlgili yazılar
React Native ve Expo WebView'lar ile mobil micro frontend mimarisi oluşturma. Gerçek dünya örnekleri ve en iyi uygulamalar.
React Native ve WebView'lar ile mobil micro frontend'ler oluşturma. Çok kanallı uygulamalar için production stratejileri ve performans optimizasyonu.
React Native Expo uygulamasına Sentry hata izleme entegrasyonu için adım adım rehber. SDK başlatma, Expo Router enstrümantasyonu, session replay, EAS Build ve EAS Update için source map yükleme ve sık karşılaşılan sorunları kapsar.
Headless CMS çözümlerinin pratik karşılaştırması - Strapi, Contentful, Kontent ve Storyblok - Cloudinary ile görsel yönetimi ve web ile mobil uygulamalar için framework entegrasyon pattern'leri.
Micro frontend'leri production'da uygulamak için pratik pattern'ler. Routing, state yönetimi, communication ve deployment stratejileri.