İçeriğe atla

Mobil Micro Frontend'ler: React Native, Expo ve WebView'lar

React Native ve Expo WebView'lar ile mobil micro frontend mimarisi oluşturma. Gerçek dünya örnekleri ve en iyi uygulamalar.

Ayhan Sipahi Ayhan Sipahi

Mobil takımlar giderek yaygınlaşan bir sorunla karşılaşır: her birinin kendi deployment döngüleri ve teknoloji stack’leri olan farklı takımlardan web tabanlı servisleri entegre etmek. Sıkı teslimat zaman dilimleri bu karmaşıklığı artırır; web takımları mobil entegrasyon için geliştirmelerini durduramaz.

Buradaki varsayılan yaklaşım, her web uygulamasını kendi WebView’ında barındıran native bir React Native kabuğu. Her web takımı kendi bundler’ını ve release temposunu korur. Bunun bedeli mobil tarafta ödenir: bellek baskısı, bundle boyutu ve elle yazmanız gereken navigasyon kodu.

Mobil Micro Frontend Serisi#

Bu, mobil micro frontend serisinin 1. Bölümü:

Serinin 2. Bölümü, WebView’lar ile native kod arasındaki iletişim köprüsünü ele alır. 3. Bölüm ise çok kanallı mimariyi ve production optimizasyonunu anlatır.

Neden Mobil Micro Frontend’ler?#

Geleneksel mobil uygulama geliştirmenin temel bir sorunu vardır: native kod deployment’ı yavaştır. App store incelemeler, kullanıcı güncelleme benimsemesi ve birden fazla takım arasında release koordinasyonu, web geliştiricilerinin yıllar önce çözdüğü darboğazlar yaratır.

Bu senaryodaki tipik kısıtlar şunlar:

  • Mevcut React/Vue/Angular uygulamaları olan 5 farklı web takımı
  • Aylık mobil release’lere karşı haftalık web deployment’ları
  • Uygulama güncellemelerini bekleyemeyen A/B test gereksinimleri
  • Anında deployment yeteneğine ihtiyaç duyan uyumluluk özellikleri

Bu kısıtlara uyan yaklaşım, web tabanlı micro frontend’leri WebView’lar aracılığıyla React Native uygulamasına gömmek.

Mimari Genel Bakış#

Üst düzey mimari şöyle:

React Native App

Native Shell

WebView Container 1

WebView Container 2

WebView Container 3

Team A: React App

Team B: Vue App

Team C: Angular App

Native Bridge

Authentication Service

Native Features

Analytics

Native uygulama şunları yapan bir shell görevi görür:

  • Kimlik doğrulama ve oturum yönetimini ele alır
  • Native işlevsellik sağlar (kamera, biyometrik, vb.)
  • Micro frontend’ler arası navigasyonu yönetir
  • WebView’lar ve native kod arasında iletişim köprüsü uygular

Değerlendirilen Alternatifler#

WebView’lar burada takım özerkliği ve release hızı sayesinde öne çıkıyor. Bu varsayılana karar vermeden önce bilinmesi gereken üç bundler seçeneği var.

Seçenek 1: Module Federation ile Re.Pack#

Re.Pack (yeni sekmede açılır), Callstack’ın Module Federation’ı React Native’e getirmek için çözümüdür. Esasen React Native’de çalışan webpack’in Module Federation’ıdır.

Örnek kurulum:

// Re.Pack konfigürasyonu: Module Federation, Re.Pack'in kendi plugin'i ile
const Repack = require('@callstack/repack');

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

Tercih edilmeme nedenleri:

  • Karmaşıklık: Mevcut webpack konfigürasyonlarında ciddi değişiklik gerektirir
  • Takım koordinasyonu: Tüm takımların aynı anda Re.Pack’e geçmesi gerekir
  • Hata ayıklama: React Native’de Module Federation hatalarını izlemek, tarayıcı build’ine göre daha zor
  • Bundle yükü: Federation runtime’ı ve paylaşılan scope, host bundle’ına ağırlık ekler

Ne zaman Re.Pack kullanılır:

  • Yeni bir projeyle sıfırdan başlıyorsanız
  • Tüm takımlar aynı bundler üzerinde koordinasyon kurabiliyorsa
  • Gerçek runtime modül paylaşımına ihtiyacınız varsa
  • Birden fazla bağımsız takımla “super app” inşa ediyorsanız

Seçenek 2: React Native ile Rspack#

Rspack (yeni sekmede açılır), webpack uyumlu bir konfigürasyon yüzeyi sunan Rust tabanlı bir bundler’dır. Micro frontend build’leri için bir başka seçenektir.

Örnek kurulum:

// rspack.config.mjs
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

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

Tercih edilmeme nedenleri:

  • React Native desteği: Rspack’in kendisi web build’lerini hedefler; React Native için üzerine Re.Pack gerekir
  • Ekosistem olgunluğu: Webpack’e kıyasla daha az plugin ve loader
  • Takım benimsemesi: Tüm web takımlarını yeni bir bundler üzerinde yeniden eğitmek gerekir
  • Production kararlılığı: Daha yeni bir araç, sıkı bir zaman çizelgesinde risk oluşturur

Ne zaman Rspack kullanılır:

  • Sadece web micro frontend’leri inşa ediyorsanız
  • Build performansı kritikse ve Rust çekirdeği ile SWC dönüşümü geçişi değerli kılıyorsa
  • Erken benimseyenlerden olmayı göze alabiliyorsanız
  • Takımlarınız Rust tabanlı araçlarla rahatsa

Seçenek 3: Vite + React Native#

Vite de hızlı HMR ve modern build sisteminden yararlanarak micro frontend build’leri için değerlendirmeye değer.

Örnek kurulum:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'date-fns']
        }
      }
    }
  },
  server: {
    cors: true
  }
});

Tercih edilmeme nedenleri:

  • Module Federation: Vite, Module Federation için topluluk plugin’ine ihtiyaç duyar ve Rollup çıktısı webpack’inkinden farklıdır
  • Production build’ler: Dev sunucusundaki hız avantajı Rollup production build’ine yansımaz
  • Plugin ekosistemi: Webpack’e özgü ihtiyaçlar için daha az plugin
  • Takım aşinalığı: Halihazırda webpack kullanan takımlar, karşılığı düşük bir migrasyon maliyeti öder

Ne zaman Vite kullanılır:

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

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

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

React Native App

Native Shell

WebView Container

Micro Frontend Loader

Team A: Webpack Build

Team B: Vite Build

Team C: Rspack Build

Shared Bridge

Authentication

Native Features

Analytics

Bu yaklaşımın güçlü yanları:

  • Takım özerkliği: Her takım tercih ettiği bundler’ı kullanır
  • Kademeli migrasyon: Takımlar daha iyi araçlara kendi takvimlerinde geçer
  • Risk azaltma: Bir build hattı bozulsa bile diğerleri çalışmaya devam eder
  • Performans: Her takım kendi çıktısını optimize eder

Uygulama#

WebView Mimarisi Kurulumu#

Expo’nun WebView’ıyla başlamak ilk sorunu hemen ortaya çıkarır. İlk implementasyon aldatıcı derecede basit görünür:

import React from 'react';
import { WebView } from 'react-native-webview';
import { SafeAreaView } from 'react-native-safe-area-context';

export function MicroFrontendContainer({ url }: { url: string }) {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <WebView
        source={{ uri: url }}
        style={{ flex: 1 }}
      />
    </SafeAreaView>
  );
}

Geliştirme ortamında bu kadarı yeterli görünür. Belleği kısıtlı Android cihazlarda ise aynı WebView, JavaScript konsolunda hiçbir iz bırakmadan boş beyaz ekran gösterebilir.

Bellek Sorunu#

Her WebView kendi tarayıcı motoru örneğini yükler; bellek tüketimi hızla artar. Aynı anda birkaç micro frontend açık kalırsa düşük donanımlı cihazlar limite dayanır. Bunu sınırlamanın bir yolu:

import React, { useRef, useCallback, useState } from 'react';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import {
  ActivityIndicator,
  AppState,
  AppStateStatus,
  Button,
  Text,
  View
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

interface MicroFrontendConfig {
  url: string;
  preload?: boolean;
  cachePolicy?: 'default' | 'reload' | 'cache-else-load';
}

class WebViewManager {
  private static instance: WebViewManager;
  private activeWebViews = new Map<string, {
    ref: React.RefObject<WebView>;
    lastUsed: number;
  }>();

  private maxWebViews = 3;

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

  registerWebView(id: string, ref: React.RefObject<WebView>): void {
    this.activeWebViews.set(id, {
      ref,
      lastUsed: Date.now()
    });

    // Limitleri aşarsak temizle
    this.cleanupIfNeeded();
  }

  private cleanupIfNeeded(): void {
    if (this.activeWebViews.size <= this.maxWebViews) return;

    // En az yakın zamanda kullanılan WebView'ı bul
    const entries = Array.from(this.activeWebViews.entries());
    const lru = entries.reduce((min, current) =>
      current[1].lastUsed < min[1].lastUsed ? current : min
    );

    // WebView'ı temizle
    lru[1].ref.current?.clearCache();
    this.activeWebViews.delete(lru[0]);

    console.log(`[WebViewManager] Bellek limitleri nedeniyle WebView ${lru[0]} temizlendi`);
  }

  updateUsage(id: string): void {
    const webView = this.activeWebViews.get(id);
    if (webView) {
      webView.lastUsed = Date.now();
    }
  }
}

export function OptimizedMicroFrontendContainer({
  config
}: {
  config: MicroFrontendConfig
}) {
  const webViewRef = useRef<WebView>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [hasError, setHasError] = useState(false);
  const webViewManager = WebViewManager.getInstance();

  const handleLoadStart = useCallback(() => {
    setIsLoading(true);
    setHasError(false);
  }, []);

  const handleLoadEnd = useCallback(() => {
    setIsLoading(false);
    webViewManager.updateUsage(config.url);
  }, [config.url]);

  const handleError = useCallback((error: any) => {
    console.error('[WebView] Yükleme hatası:', error);
    setHasError(true);
    setIsLoading(false);
  }, []);

  const handleMessage = useCallback((event: WebViewMessageEvent) => {
    // Bridge mesajlarını ele al (2. Bölümde anlatılıyor)
    console.log('[WebView] Mesaj alındı:', event.nativeEvent.data);
  }, []);

  // Manager'a kaydet
  React.useEffect(() => {
    webViewManager.registerWebView(config.url, webViewRef);
  }, [config.url]);

  // Uygulama durum değişikliklerini ele al
  React.useEffect(() => {
    const handleAppStateChange = (nextAppState: AppStateStatus) => {
      if (nextAppState === 'background') {
        // Uygulama background'a gittiğinde cache'i temizle
        webViewRef.current?.clearCache();
      }
    };

    const subscription = AppState.addEventListener('change', handleAppStateChange);
    return () => subscription?.remove();
  }, []);

  if (hasError) {
    return (
      <SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text>Micro frontend yüklenemedi</Text>
        <Button title="Tekrar Dene" onPress={() => setHasError(false)} />
      </SafeAreaView>
    );
  }

  return (
    <SafeAreaView style={{ flex: 1 }}>
      {isLoading && (
        <View style={{
          position: 'absolute',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: 'white',
          justifyContent: 'center',
          alignItems: 'center',
          zIndex: 1000
        }}>
          <ActivityIndicator size="large" />
        </View>
      )}

      <WebView
        ref={webViewRef}
        source={{
          uri: config.url,
          headers: {
            'X-Platform': 'react-native',
            'X-App-Version': '1.0.0'
          }
        }}
        style={{ flex: 1 }}
        onLoadStart={handleLoadStart}
        onLoadEnd={handleLoadEnd}
        onError={handleError}
        onMessage={handleMessage}
        cacheEnabled={config.cachePolicy !== 'reload'}
        cacheMode={config.cachePolicy === 'cache-else-load' ? 'LOAD_CACHE_ELSE_NETWORK' : 'LOAD_DEFAULT'}
        // Performans için kritik
        javaScriptEnabled={true}
        domStorageEnabled={true}
        startInLoadingState={true}
        scalesPageToFit={true}
        // Güvenlik
        allowsInlineMediaPlayback={false}
        mediaPlaybackRequiresUserAction={true}
        // Performans
        removeClippedSubviews={true}
        overScrollMode="never"
      />
    </SafeAreaView>
  );
}

Bundle Boyutu Sorunu#

İlk micro frontend bundle’ları çoğu zaman her biri 2-3MB’a ulaşır; bu da yavaş yükleme sürelerine ve kötü kullanıcı deneyimine neden olur. Optimize etmenin yolu:

// webpack.config.js - WebView teslimatı için optimize edilmiş
const { ModuleFederationPlugin } = require('webpack').container;
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5
        }
      }
    },
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // console.log'ları kaldır
            drop_debugger: true
          },
          mangle: {
            safari10: true // Safari 10 sorunlarını düzelt
          }
        }
      })
    ]
  },
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    }),
    new ModuleFederationPlugin({
      name: 'micro-frontend',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App.tsx'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    })
  ]
};

Alternatif: Rspack Konfigürasyonu#

Rspack’i denemek isteyen takımlar için, işte eşdeğer konfigürasyon:

// rspack.config.mjs - Rspack versiyonu
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default {
  entry: './src/index.tsx',
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        }
      }
    }
  },
  module: {
    rules: [
      {
        test: /\.tsx$/,
        use: {
          loader: 'builtin:swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'typescript',
                tsx: true
              },
              transform: {
                react: {
                  runtime: 'automatic'
                }
              },
              minify: {
                compress: {
                  drop_console: true,
                  drop_debugger: true
                }
              }
            }
          }
        }
      }
    ]
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'micro-frontend',
      filename: 'remoteEntry.js',
      exposes: {
        './App': './src/App.tsx'
      }
    })
  ]
};

İki konfigürasyon da benzer çıktı üretir; geçiş büyük ölçüde mekaniktir, loader zinciri ve split-chunks ayarları olduğu gibi taşınır. Build süresindeki fark Rust çekirdeği ile SWC dönüşümünden gelir ve modül sayısına, loader zincirine göre değişir. Bu yüzden migrasyona karar vermeden önce kendi projenizde ölçün.

WebView navigasyonu native navigasyon gibi çalışmaz. Kullanıcılar geri butonunun çalışmasını bekler, ama WebView’ların kendi geçmişi vardır. İşte çalışan bir çözüm:

import React from 'react';
import { useFocusEffect } from '@react-navigation/native';
import { BackHandler } from 'react-native';
import type { WebView } from 'react-native-webview';

export function WebViewWithNavigation({
  webViewRef,
  canGoBack,
  onGoBack
}: {
  webViewRef: React.RefObject<WebView>;
  canGoBack: boolean;
  onGoBack: () => void;
}) {
  useFocusEffect(
    React.useCallback(() => {
      const onBackPress = () => {
        if (canGoBack) {
          // Önce WebView'da geri gitmeye çalış
          webViewRef.current?.goBack();
          return true; // Varsayılan geri davranışını önle
        } else {
          // Native navigasyonun ele almasına izin ver
          onGoBack();
          return false;
        }
      };

      const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);

      return () => subscription.remove();
    }, [canGoBack, onGoBack])
  );

  return null;
}

Varsayılanın Sınırları#

WebView micro frontend’leri şu koşullarda tutar: ihtiyaç duyduğunuz özellikler zaten web uygulaması olarak var, web takımları mobil release treninden hızlı ilerliyor ve ekranlar içerik ya da form ağırlıklı. Bedeli de yukarıda anlatılan işler: WebView bütçesi, bundle diyeti ve platformun hazır sunmadığı geri tuşu davranışını elle kurmak.

İki durumda başka bir yol gerekir. Sürekli 60fps etkileşim, native jestler veya offline-first depolama isteyen ekranlar native React Native tarafına aittir. Tüm takımlar tek bir bundler ve tek bir React sürümünde buluşabiliyorsa, Module Federation ile Re.Pack izole tarayıcı örnekleri açmak yerine runtime modüllerini paylaşır; super app için daha iyi takas budur.

Sonraki Bölüm#

2. Bölüm şunları ele alır:

  • WebView’lar ve native kod arasında sağlam iletişim köprüsü kurma
  • Type-safe mesaj geçişi
  • Kimlik doğrulama ve native özellikleri ele alma
  • Hata ayıklama ve izleme stratejileri

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

Bu serideki tüm yazılar

İlgili yazılar