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ü:
- Kısım 1 (Burada bulunuyorsunuz): Mimari temelleri ve WebView entegrasyon kalıpları
- Kısım 2: WebView iletişim kalıpları ve servis entegrasyonu
- Kısım 3: Çok kanallı mimari ve production optimizasyonu
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:
// 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:
// 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:
// 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:
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:
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:
// 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
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:
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#
-
WebView'lar uygulanabilir ama optimizasyon gerektirir: Micro frontend'ler için iyi çalışırlar ama dikkatli bellek ve performans yönetimi gerektirir.
-
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.
-
Rspack umut verici: Yeni projeler için hızı ve webpack uyumluluğu nedeniyle Rspack'i düşünün.
-
Re.Pack güçlü ama karmaşık: Super app'ler için harika ama önemli koordinasyon gerektirir.
-
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.
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!
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!