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.

Geçen yıl takımımız giderek yaygınlaşan bir meydan okumayla karşılaştı: farklı takımlardan gelen, her birinin kendi deployment döngüleri ve teknoloji stack'leri olan birden fazla web tabanlı servisi entegre edebilecek bir mobil uygulama oluşturmamız gerekiyordu. Yakalayıcı nokta? Teslim etmek için 8 haftamız vardı ve web takımları bize yardım etmek için özellik geliştirmelerini durduramazdı.

İşte o zaman WebView'ları kullanarak React Native uygulamamıza micro frontend mimarisi getirmeye karar verdik. Ardından gelen 6 ay hata ayıklama, performans optimizasyonu ve hiç uygulayacağımı düşünmediğim yaratıcı çözümlerle doluydu. İşte öğrendiklerimiz.

Mobil Micro Frontend Serisi#

Bu, mobil micro frontend'ler üzerine kapsamlı 3 bölümlük serinin 1. Bölümü:

Mobil micro frontend'lere yeni misiniz? Mimari ve uygulama temellerini anlamak için buradan başlayın.

Uygulamaya hazır mısınız? İletişim kalıpları için Kısım 2'ye geçin.

Production'da çalışıyor musunuz? Optimizasyon stratejileri için Kısım 3'ü kontrol edin.

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.

Bizim özel kısıtlarımız şunlardı:

  • 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

Çözüm? WebView'ları kullanarak React Native uygulamamıza web tabanlı micro frontend'leri gömme.

Mimari Genel Bakış#

İşte uyguladığımız üst düzey mimari:

Loading diagram...

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

Alternatif Yaklaşımlar: Geleneksel WebView'ların Ötesinde#

WebView implementasyonumuza dalmadan önce, değerlendirdiğimiz alternatif yaklaşımları ve neden bunlar yerine WebView'ları seçtiğimizi paylaşayım.

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

Re.Pack, 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.

Denediğimiz şey:

TypeScript
// Re.Pack konfigürasyonu
const { ModuleFederationPlugin } = require('@module-federation/nextjs-mf');

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

Neden seçmedik:

  • Karmaşıklık: Mevcut webpack konfigürasyonlarımızda önemli değişiklikler gerektiriyordu
  • Takım koordinasyonu: Tüm takımların aynı anda Re.Pack'i benimser gerekiyordu
  • Hata ayıklama: React Native'de Module Federation hata ayıklama hala olgunlaşmamıştı
  • Performans: Federation overhead'ı nedeniyle ilk bundle boyutu 40% arttı

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, webpack uyumlu ama çok daha hızlı Rust tabanlı bir bundler'dır. Micro frontend build'lerimiz için Rspack kullanmayı denedik.

Denediğimiz şey:

TypeScript
// rspack.config.mjs
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'
      }
    })
  ]
};

Neden seçmedik:

  • React Native uyumluluğu: Rspack'in henüz native React Native desteği yok
  • Ekosistem olgunluğu: Webpack'e kıyasla daha az plugin ve loader
  • Takım benimsenemesi: 5 takımı yeni bir bundler üzerinde yeniden eğitmeyi gerektirecekti
  • Production kararlılığı: Daha yeni bir araçta zaman çizelgemizi riske atamazdık

Ne zaman Rspack kullanılır:

  • Sadece web micro frontend'leri inşa ediyorsanız
  • Build performansı kritikse (Rspack webpack'ten 10x daha hızlı)
  • Erken benimser olmayı göze alabilirserniz
  • Takımlarınız Rust tabanlı araçlarla rahatta

Seçenek 3: Vite + React Native#

Ayrıca hızlı HMR ve modern build sisteminden yararlanarak micro frontend build'lerimiz için Vite kullanmayı değerlendirdik.

Denediğimiz şey:

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

Neden seçmedik:

  • Module Federation: Vite'ın Module Federation desteği hala deneyseldi
  • Production build'ler: Production build'lerimiz webpack'ten daha yavaştı
  • Plugin ekosistemi: Özel ihtiyaçlarımız için daha az plugin
  • Takım aşinalığı: Takımlar webpack ile daha rahattı

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 (Gerçekte Yaptığımız)#

Tüm seçenekleri değerlendirdikten sonra, bize tüm dünyaların en iyisini veren hibrit bir yaklaşım seçtik:

Loading diagram...

Neden bu işe yaradı:

  • Takım özerkliği: Her takım tercih ettiği bundler'ı kullanabiliyordu
  • Kademeli migrasyon: Takımlar zaman içinde daha iyi araçlara migrate olabiliyordu
  • Risk azaltması: Bir yaklaşım başarısız olsa, diğerleri çalışmaya devam ediyordu
  • Performans: Her takım kendi build'lerini optimize edebiliyordu

Uygulama: Gerçek Hikaye#

WebView Mimarisini Kurma#

Expo'nun WebView'ıyla başladık, ama hızlıca ilk meydan okumamızla karşılaştık. İlk implementasyon aldatıcı derecede basit görünüyordu:

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

Bu geliştirme ortamında tam 5 dakika çalıştı, sonra ilk production sorunumuza çarptık: WebView, 4GB'den az RAM'i olan Android cihazlarda rastgele beyaz ekran gösteriyordu.

Bellek Sorunu#

Crash raporlama ekledikten sonra, WebView'ların her birinin 150-200MB tükettiğini keşfettik. 3 micro frontend yüklendiğinde, düşük seviye cihazlarda bellek limitlerini aşıyorduk. İşte nasıl çözdük:

TypeScript
import React, { useRef, useCallback, useState } from 'react';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { AppState, AppStateStatus } from 'react-native';

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;
    memoryUsage: number;
  }>();

  private maxWebViews = 3;
  private maxMemoryPerWebView = 100 * 1024 * 1024; // 100MB

  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(),
      memoryUsage: 0
    });

    // 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 (Kısım 2'de kapsanacak)
    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ımız devasaydı - her biri 2-3MB. Bu yavaş yükleme süreleri ve kötü kullanıcı deneyimine neden oluyordu. İşte nasıl optimize ettik:

TypeScript
// 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:

TypeScript
// rspack.config.mjs - Rspack versiyonu
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'
      }
    })
  ]
};

Performans karşılaştırması:

  • Webpack build süresi: 45 saniye
  • Rspack build süresi: 8 saniye (82% daha hızlı)
  • Bundle boyutu: Benzer (5% içinde)
  • Bellek kullanımı: Rspack build'ler sırasında 30% daha az bellek kullandı

Navigasyon Sorunu#

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 çözümümüz:

TypeScript
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { BackHandler } from 'react-native';

export function WebViewWithNavigation({
  webViewRef,
  canGoBack,
  onGoBack
}: {
  webViewRef: React.RefObject<WebView>;
  canGoBack: boolean;
  onGoBack: () => void;
}) {
  const navigation = useNavigation();

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

      BackHandler.addEventListener('hardwareBackPress', onBackPress);

      return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
    }, [canGoBack, onGoBack])
  );

  return null;
}

Performans Sonuçları#

Bu optimizasyonları uyguladıktan sonra, metriklerimiz dramatik olarak iyileşti:

Yükleme Performansı#

  • İlk yükleme süresi: 2.3s → 0.8s (65% iyileşme)
  • Bundle boyutu: 2.8MB → 1.2MB (57% azalma)
  • Bellek kullanımı: WebView başına 200MB → 120MB (40% azalma)

Kullanıcı Deneyimi#

  • Uygulama çökmeleri: 2.3% → 0.1% (96% azalma)
  • Beyaz ekran olayları: 15% → 1% (93% azalma)
  • Kullanıcı memnuniyet puanı: 3.2 → 4.6 (44% iyileşme)

Build Performansı#

  • Webpack build'leri: 45s → 35s (22% daha hızlı)
  • Rspack build'leri: 8s (webpack'ten 82% daha hızlı)
  • Geliştirme HMR: 2s → 0.3s (85% daha hızlı)

Anahtar Öğretiler#

  1. WebView'lar uygulanabilir ama optimizasyon gerektirir: Micro frontend'ler için iyi çalışırlar ama dikkatli bellek ve performans yönetimi gerektirir.

  2. Hibrit yaklaşımlar en iyi çalışır: Takımların tercih ettikleri araçları kullanmalarına izin verirken tutarlı bir arayüz sürdürün.

  3. Rspack umut verici: Yeni projeler için hızı ve webpack uyumluluğu nedeniyle Rspack'i düşünün.

  4. Re.Pack güçlü ama karmaşık: Super app'ler için harika ama önemli koordinasyon gerektirir.

  5. Performans kritik: Kullanıcılar yavaş WebView'ları hemen fark eder. Agresif optimize edin.

Sırada Ne Var?#

Kısım 2'de şunlara dalacağız:

  • 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

Temel çok önemlidir. Mimarimizi doğru kurarsanız, diğer her şey yönetilebilir hale gelir. Yanlış kurarsanız, sonsuza kadar performans sorunlarıyla mücadele edersiniz.

Bir dahaki sefer, WebView'ları ve native kodun birbirleriyle güvenilir bir şekilde nasıl konuşturulacağına ve yol boyunca çözdüğümüz hata ayıklama kaburlara bakacağız.

Loading...

Yorumlar (0)

Sohbete katıl

Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap

Henüz yorum yok

Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!

Related Posts