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.
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ü:
- 1. Bölüm (Buradasınız): Mimari temelleri ve WebView entegrasyon kalıpları
- 2. Bölüm: WebView iletişim kalıpları ve servis entegrasyonu
- 3. Bölüm: Çok kanallı mimari ve production optimizasyonu
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:
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:
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.
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 ç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-webview - GitHub (yeni sekmede açılır) - React Native için topluluk tarafından geliştirilen çapraz platform WebView bileşeni; iletişim ve injection kalıplarını kapsayan derinlemesine rehber içerir
- react-native-webview Kullanım Rehberi (yeni sekmede açılır) - JavaScript injection ve mesaj geçişi dahil en yaygın WebView kullanım senaryolarını ele alan pratik rehber
- Expo WebView Dokümantasyonu (yeni sekmede açılır) - Expo’nun react-native-webview entegrasyonu için platform-spesifik notlar dahil referans dokümantasyonu
- Re.Pack - Mikro Frontend’ler (yeni sekmede açılır) - Module Federation ile React Native’de mikro frontend mimarileri oluşturmak için Re.Pack rehberi
- Micro Frontends - Martin Fowler (yeni sekmede açılır) - Cam Jackson tarafından yazılmış, micro frontend mimari kalıbı üzerine temel makale
- React Native Performans Genel Bakış (yeni sekmede açılır) - JavaScript thread performansı, bellek yönetimi ve optimizasyon stratejileri üzerine resmi dokümantasyon
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.
Bu serideki tüm yazılar
İlgili yazılar
React Native ve WebView'lar ile mobil micro frontend'ler oluşturma. Çok kanallı uygulamalar için production stratejileri ve performans optimizasyonu.
expo · performance · re-pack +4
WebView-native iletişim pattern'leri: mesaj geçişi, servis entegrasyonu ve çalışan kodla tipli bir request-response bridge'i.
expo · mobile · build-tools +4
Sunucu güdümlü arayüz, sunucu tarafı kompozisyonun mobil karşılığıdır. Zor kısmı JSON render etmek değil, eski sürümlerde ayakta kalan sürümlenmiş bileşen sözleşmesidir.
mobile · react-native · architecture
React Native Expo uygulamasına Sentry ekleme rehberi: SDK kurulumu, Expo Router enstrümantasyonu, session replay ve EAS için source map yükleme.
react-native · expo · monitoring +2
Headless CMS çözümlerinin (Strapi, Contentful, Kontent, Storyblok) pratik karşılaştırması: Cloudinary ile görsel yönetimi ve framework entegrasyonu.
typescript · nextjs · react-native +3