Micro Frontend Uygulama Pattern'leri: Pratik Rehber
Micro frontend'leri production'da uygulamak için pratik pattern'ler. Routing, state yönetimi, communication ve deployment stratejileri.
Micro Frontend Serisi Navigasyonu#
- Bölüm 1: Mimari temelleri ve uygulama türleri
- Bölüm 2 (Şu anda buradasınız): Module Federation, iletişim pattern'leri ve entegrasyon stratejileri
- Bölüm 3: İleri düzey pattern'ler, performans optimizasyonu ve production hata ayıklama
Ön Koşullar: Bu yazı Bölüm 1'deki kavramlar üzerine kuruludur. Micro frontend'lere yeniyseniz, önce oradan başlayın.
Bölüm 1'de micro frontend'ler için temel mimari pattern'leri keşfettik. Şimdi pratik uygulamaya derinlemesine dalacağız, runtime entegrasyonunda baskın yaklaşım olan Module Federation'a odaklanacağız, ayrıca production sistemlerinde karşılaştığım gerçek dünya iletişim pattern'leri ve hata ayıklama stratejilerini paylaşacağım.
Module Federation Derinlemesine İnceleme#
Webpack 5'te tanıtılan Module Federation, runtime micro frontend entegrasyonu için altın standart haline geldi. Basit dinamik import'lardan farklı olarak, sofistike bağımlılık paylaşımı, versiyon yönetimi ve runtime kompozisyon yetenekleri sağlar.
Production'a Hazır Module Federation Sistemi Kurma#
Farklı takımların farklı domain'lere sahip olduğu gerçekçi bir e-ticaret uygulaması oluşturalım:
// apps/shell/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
remotes: {
// Ürün takımının micro frontend'i
products: 'products@http://localhost:3001/remoteEntry.js',
// Sepet takımının micro frontend'i
cart: 'cart@http://localhost:3002/remoteEntry.js',
// Kullanıcı takımının micro frontend'i
user: 'user@http://localhost:3003/remoteEntry.js',
},
shared: {
react: {
singleton: true,
strictVersion: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
strictVersion: true,
requiredVersion: '^18.2.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.8.0',
},
// Özel paylaşılan yardımcılar
'@company/design-system': {
singleton: true,
requiredVersion: '^2.1.0',
},
'@company/event-bus': {
singleton: true,
requiredVersion: '^1.0.0',
}
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
devServer: {
port: 3000,
historyApiFallback: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
// apps/products/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
entry: './src/index.ts',
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./ProductSearch': './src/components/ProductSearch',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.8.0',
},
'@company/design-system': {
singleton: true,
requiredVersion: '^2.1.0',
},
'@company/event-bus': {
singleton: true,
requiredVersion: '^1.0.0',
}
},
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
devServer: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
Error Boundary'ler ile Güçlü Modül Yükleme#
Module Federation'da en büyük zorluklardan biri yükleme hatalarını zarif bir şekilde ele almaktır. İşte production'da test edilmiş bir yaklaşım:
// src/components/MicroFrontendLoader.tsx
import React, { Suspense, lazy, useState, useEffect } from 'react';
interface MicroFrontendConfig {
scope: string;
module: string;
url: string;
fallback?: React.ComponentType;
}
interface LoadingState {
isLoading: boolean;
error: Error | null;
retryCount: number;
}
const useDynamicScript = (url: string) => {
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState(false);
useEffect(() => {
if (!url) return;
const element = document.createElement('script');
element.src = url;
element.type = 'text/javascript';
element.async = true;
setReady(false);
setFailed(false);
element.onload = () => {
console.log(`Dinamik Script Yüklendi: ${url}`);
setReady(true);
};
element.onerror = () => {
console.error(`Dinamik Script Hatası: ${url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(element);
return () => {
console.log(`Dinamik Script Kaldırıldı: ${url}`);
document.head.removeChild(element);
};
}, [url]);
return { ready, failed };
};
const loadComponent = (scope: string, module: string) => {
return async () => {
// Paylaşım kapsamını başlatır. Bu, bu build'den ve tüm remote'lardan bilinen sağlanan modüllerle doldurur
await __webpack_init_sharing__('default');
const container = (window as any)[scope]; // veya container'ı başka bir yerden alın
if (!container) {
throw new Error(`Container '${scope}' bulunamadı`);
}
// Container'ı başlat, paylaşılan modüller sağlayabilir
await container.init(__webpack_share_scopes__.default);
const factory = await (window as any)[scope].get(module);
const Module = factory();
return Module;
};
};
class MicroFrontendErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ComponentType },
{ hasError: boolean; error: Error | null }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error('Micro Frontend Hatası:', error, errorInfo);
// İzleme servisine gönder
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Micro Frontend Hatası', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
}
render() {
if (this.state.hasError) {
const Fallback = this.props.fallback;
return <Fallback />;
}
return this.props.children;
}
}
export const MicroFrontendLoader: React.FC<{
config: MicroFrontendConfig;
props?: Record<string, any>;
}> = ({ config, props = {} }) => {
const { ready, failed } = useDynamicScript(config.url);
const [loadingState, setLoadingState] = useState<LoadingState>({
isLoading: false,
error: null,
retryCount: 0,
});
useEffect(() => {
if (ready && !loadingState.isLoading) {
setLoadingState(prev => ({ ...prev, isLoading: true, error: null }));
}
}, [ready]);
const handleRetry = () => {
if (loadingState.retryCount <3) {
setLoadingState(prev => ({
...prev,
retryCount: prev.retryCount + 1,
error: null,
isLoading: true,
}));
// Script'i yeniden yüklemeye zorla
window.location.reload();
}
};
if (failed || (loadingState.error && loadingState.retryCount >= 3)) {
const Fallback = config.fallback || DefaultErrorFallback;
return <Fallback onRetry={handleRetry} />;
}
if (!ready) {
return <LoadingFallback />;
}
const Component = lazy(loadComponent(config.scope, config.module));
return (
<MicroFrontendErrorBoundary fallback={config.fallback || DefaultErrorFallback}>
<Suspense fallback={<LoadingFallback />}>
<Component {...props} />
</Suspense>
</MicroFrontendErrorBoundary>
);
};
const LoadingFallback: React.FC = () => (
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-1/2"></div>
</div>
);
const DefaultErrorFallback: React.FC<{ onRetry?: () => void }> = ({ onRetry }) => (
<div className="p-4 border border-red-300 rounded bg-red-50">
<h3 className="text-red-800 font-semibold">Bir şeyler yanlış gitti</h3>
<p className="text-red-600 text-sm mt-1">
Bu bölüm yüklenemedi. Lütfen sayfayı yenilemeyi deneyin.
</p>
{onRetry && (
<button
onClick={onRetry}
className="mt-2 px-3 py-1 bg-red-600 text-white rounded text-sm"
>
Tekrar Dene
</button>
)}
</div>
);
Cross-Micro Frontend İletişimi#
Micro frontend mimarisinin en zorlu yönlerinden biri, bağımsız olarak geliştirilmiş ve dağıtılmış uygulamalar arasında iletişimi sağlamaktır. İşte en etkili bulduğum pattern'ler:
1. Event-Driven İletişim#
// @company/event-bus - Paylaşılan event bus paketi
interface EventBusEvent {
type: string;
payload: any;
source: string;
timestamp: number;
}
class EventBus {
private listeners: Map<string, Array<(event: EventBusEvent) => void>> = new Map();
private eventHistory: EventBusEvent[] = [];
private maxHistorySize = 50;
subscribe(eventType: string, callback: (event: EventBusEvent) => void): () => void {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType)!.push(callback);
// Unsubscribe fonksiyonunu döndür
return () => {
const callbacks = this.listeners.get(eventType);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
publish(type: string, payload: any, source: string = 'unknown') {
const event: EventBusEvent = {
type,
payload,
source,
timestamp: Date.now(),
};
// Geçmişe ekle
this.eventHistory.push(event);
if (this.eventHistory.length > this.maxHistorySize) {
this.eventHistory.shift();
}
// Dinleyicileri bilgilendir
const callbacks = this.listeners.get(type) || [];
callbacks.forEach(callback => {
try {
callback(event);
} catch (error) {
console.error(`${type} için event listener'da hata:`, error);
}
});
// Development'ta debug loglama
if (process.env.NODE_ENV === 'development') {
console.log(`[EventBus] ${type}:`, payload);
}
}
getHistory(eventType?: string): EventBusEvent[] {
if (eventType) {
return this.eventHistory.filter(event => event.type === eventType);
}
return [...this.eventHistory];
}
// Geç yüklenen micro frontend'ler için event'leri tekrar oynat
replayEvents(eventType: string, callback: (event: EventBusEvent) => void) {
const pastEvents = this.eventHistory.filter(event => event.type === eventType);
pastEvents.forEach(event => callback(event));
}
}
export const eventBus = new EventBus();
// Daha kolay kullanım için React hook
export const useEventBus = (
eventType: string,
callback: (event: EventBusEvent) => void,
deps: React.DependencyList = []
) => {
useEffect(() => {
const unsubscribe = eventBus.subscribe(eventType, callback);
return unsubscribe;
}, deps);
const publish = useCallback((payload: any, source?: string) => {
eventBus.publish(eventType, payload, source);
}, [eventType]);
return { publish };
};
2. Micro Frontend'lerde Pratik Kullanım#
// products/src/components/ProductList.tsx
import React, { useState, useEffect } from 'react';
import { useEventBus } from '@company/event-bus';
interface Product {
id: string;
name: string;
price: number;
image: string;
}
export const ProductList: React.FC = () => {
const [products, setProducts] = useState<Product[]>([]);
const [filters, setFilters] = useState<any>(null);
// Arama micro frontend'inden filtre değişikliklerini dinle
useEventBus('search:filters-changed', (event) => {
setFilters(event.payload);
});
// Geri bildirim göstermek için sepet güncellemelerini dinle
useEventBus('cart:item-added', (event) => {
// Başarı bildirimi göster
showNotification(`${event.payload.productName} sepete eklendi!`);
});
const { publish } = useEventBus('products:product-selected', () => {});
const handleProductClick = (product: Product) => {
publish({
productId: product.id,
productName: product.name,
source: 'product-list',
}, 'products');
};
useEffect(() => {
// Değiştiğinde filtreleri uygula
if (filters) {
// Ürünleri filtreleme mantığı
const filtered = applyFilters(products, filters);
setProducts(filtered);
}
}, [filters]);
return (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
{products.map(product => (
<div
key={product.id}
className="border rounded-lg p-4 cursor-pointer hover:shadow-lg"
onClick={() => handleProductClick(product)}
>
<img src={product.image} alt={product.name} className="w-full h-48 object-cover" />
<h3 className="font-semibold mt-2">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</div>
))}
</div>
);
};
// cart/src/components/CartButton.tsx
import React, { useState, useEffect } from 'react';
import { useEventBus } from '@company/event-bus';
export const CartButton: React.FC = () => {
const [itemCount, setItemCount] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
// Ürün eklemelerini dinle
useEventBus('products:product-selected', (event) => {
addToCart(event.payload.productId);
});
const { publish } = useEventBus('cart:item-added', () => {});
const addToCart = async (productId: string) => {
try {
// Sepete ekleme mantığı
const response = await fetch('/api/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId }),
});
if (response.ok) {
const result = await response.json();
setItemCount(prev => prev + 1);
// Animasyonu tetikle
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 500);
// Diğer micro frontend'leri bilgilendir
publish({
productId,
productName: result.productName,
newTotal: result.cartTotal,
}, 'cart');
}
} catch (error) {
console.error('Sepete ekleme başarısız:', error);
}
};
return (
<button
className={`relative p-2 bg-blue-600 text-white rounded-full ${
isAnimating ? 'animate-pulse' : ''
}`}
>
<ShoppingCartIcon className="w-6 h-6" />
{itemCount > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{itemCount}
</span>
)}
</button>
);
};
Routing Stratejileri#
Micro frontend mimarilerinde routing, çakışmaları önlemek ve sorunsuz bir kullanıcı deneyimi sağlamak için dikkatli koordinasyon gerektirir:
1. Shell Kontrollü Routing#
// shell/src/App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { MicroFrontendLoader } from './components/MicroFrontendLoader';
const App: React.FC = () => {
return (
<BrowserRouter>
<div className="min-h-screen bg-gray-50">
{/* Global navigasyon */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16">
<div className="flex">
<Link to="/" className="flex items-center px-4 text-lg font-semibold">
E-Ticaret
</Link>
<div className="flex space-x-8 ml-8">
<Link to="/products" className="flex items-center px-3 py-2 hover:text-blue-600">
Ürünler
</Link>
<Link to="/categories" className="flex items-center px-3 py-2 hover:text-blue-600">
Kategoriler
</Link>
</div>
</div>
{/* Kullanıcı hesabı micro frontend */}
<div className="flex items-center">
<MicroFrontendLoader
config={{
scope: 'user',
module: './UserMenu',
url: 'http://localhost:3003/remoteEntry.js',
}}
/>
</div>
</div>
</nav>
{/* Ana içerik alanı */}
<main className="max-w-7xl mx-auto px-4 py-8">
<Routes>
<Route path="/" element={<Navigate to="/products" replace />} />
{/* Products micro frontend tüm /products/* route'larını yönetir */}
<Route
path="/products/*"
element={
<MicroFrontendLoader
config={{
scope: 'products',
module: './ProductsApp',
url: 'http://localhost:3001/remoteEntry.js',
}}
/>
}
/>
{/* Cart micro frontend */}
<Route
path="/cart/*"
element={
<MicroFrontendLoader
config={{
scope: 'cart',
module: './CartApp',
url: 'http://localhost:3002/remoteEntry.js',
}}
/>
}
/>
{/* Bilinmeyen route'lar için fallback */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</div>
</BrowserRouter>
);
};
2. Micro Frontend İç Routing#
// products/src/ProductsApp.tsx
import React from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { ProductList } from './components/ProductList';
import { ProductDetail } from './components/ProductDetail';
import { ProductSearch } from './components/ProductSearch';
export const ProductsApp: React.FC = () => {
const location = useLocation();
// Micro frontend için analytics takibi
useEffect(() => {
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.page('Products', {
path: location.pathname,
microfrontend: 'products',
});
}
}, [location.pathname]);
return (
<div className="products-app">
<Routes>
{/* Not: path'ler /products'a göreceli */}
<Route index element={<ProductList />} />
<Route path="search" element={<ProductSearch />} />
<Route path="category/:categoryId" element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Routes>
</div>
);
};
Hata Ayıklama Hikayesi: Kaybolan Route'ların Gizemi#
İşte micro frontend routing'in karmaşıklığını gösteren gerçek bir hata ayıklama zorluğu. Belirli ürün detay sayfalarının rastgele 404 hatası gösterdiği bir production sorunumuz vardı, ancak sadece bazı kullanıcılar için ve sadece bazen.
Araştırma, routing kurulumumuzda bir yarış durumu olduğunu ortaya çıkardı:
// Sorunlu kod
const ProductsApp: React.FC = () => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
// Bu yarış durumuna neden oluyordu
setTimeout(() => setIsLoaded(true), 100);
}, []);
if (!isLoaded) {
return <div>Yükleniyor...</div>;
}
return (
<Routes>
<Route path=":productId" element={<ProductDetail />} />
</Routes>
);
};
Sorun, shell'deki React Router'ın micro frontend kendi route'larını başlatmayı bitirmeden önce route'ları eşleştirmeye çalışmasıydı. Daha hızlı bağlantıya sahip kullanıcılar yarış durumuna daha sık çarpıyordu.
Çözüm, shell ve micro frontend routing arasında koordinasyon gerektirdi:
// Uygun route kaydı ile düzeltilmiş versiyon
import { useEventBus } from '@company/event-bus';
const ProductsApp: React.FC = () => {
const [routesReady, setRoutesReady] = useState(false);
const { publish } = useEventBus('routing:micro-frontend-ready', () => {});
useEffect(() => {
// Mevcut route'ları shell'e kaydet
publish({
microfrontend: 'products',
routes: [
'/products',
'/products/search',
'/products/category/:categoryId',
'/products/:productId'
]
}, 'products');
setRoutesReady(true);
}, [publish]);
if (!routesReady) {
return <LoadingSpinner />;
}
return (
<Routes>
<Route index element={<ProductList />} />
<Route path="search" element={<ProductSearch />} />
<Route path="category/:categoryId" element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Routes>
);
};
Bu deneyim bize şunların önemini öğretti:
- Shell ve micro frontend'ler arasında açık route kaydı
- Routing'e müdahale etmeyen uygun yükleme durumları
- Production'da route çözümlemenin kapsamlı izlenmesi
Geliştirme ve Test Stratejileri#
Yerel Geliştirme Kurulumu#
// scripts/dev-all.js - Tüm micro frontend'leri yerel olarak çalıştırmak için script
const { spawn } = require('child_process');
const path = require('path');
const services = [
{ name: 'shell', port: 3000, path: './apps/shell' },
{ name: 'products', port: 3001, path: './apps/products' },
{ name: 'cart', port: 3002, path: './apps/cart' },
{ name: 'user', port: 3003, path: './apps/user' },
];
const processes = [];
services.forEach(service => {
console.log(`${service.name} ${service.port} portunda başlatılıyor...`);
const process = spawn('npm', ['run', 'dev'], {
cwd: path.resolve(service.path),
stdio: 'inherit',
shell: true,
env: { ...process.env, PORT: service.port.toString() }
});
processes.push(process);
});
// Zarif kapanış
process.on('SIGTERM', () => {
processes.forEach(p => p.kill());
});
process.on('SIGINT', () => {
processes.forEach(p => p.kill());
process.exit(0);
});
Entegrasyon Testi#
// tests/integration/micro-frontend-integration.test.ts
import { test, expect, Page } from '@playwright/test';
test.describe('Micro Frontend Entegrasyonu', () => {
test('tüm micro frontend\'leri doğru yüklemeli', async ({ page }) => {
await page.goto('http://localhost:3000');
// Shell'in yüklenmesini bekle
await expect(page.locator('[data-testid="shell-loaded"]')).toBeVisible();
// Micro frontend'lerin yüklendiğini kontrol et
await expect(page.locator('[data-testid="products-mf"]')).toBeVisible();
await expect(page.locator('[data-testid="user-menu-mf"]')).toBeVisible();
});
test('micro frontend iletişimini yönetmeli', async ({ page }) => {
await page.goto('http://localhost:3000/products');
// Bir ürüne tıkla
await page.click('[data-testid="product-card"]:first-child');
// Sepetin güncellendiğini doğrula
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
// Bildirimin göründüğünü doğrula
await expect(page.locator('[data-testid="notification"]')).toContainText('sepete eklendi');
});
test('micro frontend hatalarını zarif bir şekilde ele almalı', async ({ page }) => {
// Bir micro frontend için ağ hatasını simüle et
await page.route('**/products/remoteEntry.js', route => {
route.abort();
});
await page.goto('http://localhost:3000');
// Fallback UI göstermeli
await expect(page.locator('[data-testid="products-fallback"]')).toBeVisible();
// Diğer micro frontend'ler hala çalışmalı
await expect(page.locator('[data-testid="user-menu-mf"]')).toBeVisible();
});
});
Sıradaki Konular#
Artık Module Federation ile micro frontend'leri uygulamak için temel bilgilere sahipsiniz, ayrıca güçlü iletişim ve routing pattern'lerine de sahipsiniz. Ancak production sistemleri daha sofistike yaklaşımlar gerektirir.
Bölüm 3: İleri Düzey Pattern'ler, Performans ve Hata Ayıklama için devam edin:
- Dağıtık frontend'ler arasında ileri düzey state yönetimi
- Performans optimizasyonu ve bundle analiz teknikleri
- Production hata ayıklama hikayeleri ve izleme stratejileri
- Cross-origin iletişim için güvenlik pattern'leri
- Bellek sızıntısı tespiti ve çözümü
- Monolith'lerden migrasyon stratejileri
Önemli İçgörü: Burada ele alınan pattern'ler çoğu kullanım durumu için iyi çalışır, ancak gerçek zorluklar ölçek, karmaşık state etkileşimleri ve production ortamlarındaki kaçınılmaz edge case'lerle uğraşırken ortaya çıkar.
Paylaşılan hata ayıklama hikayeleri karşılaşacağınız yaygın sorunları temsil eder. Her micro frontend sistemi benzersiz karmaşıklıklar getirir, ancak bu temel pattern'leri anlamak onları daha etkili bir şekilde yönetmenize yardımcı olacaktır.
Seri Navigasyonu
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!