Vue Router ile Navigation Guards ve Route Yönetimi
Vue Router'ın navigation guard'ları, web uygulamalarında route geçişlerini kontrol etmek ve yönetmek için güçlü bir mekanizma sunar. Bu yazıda, navigation guard'ları derinlemesine inceleyeceğiz ve gerçek dünya senaryolarında nasıl kullanılacağını öğreneceğiz.
Navigation Guard'lar Nedir?
Navigation guard'lar, route değişimlerini kontrol eden ve yönlendiren fonksiyonlardır. Bu guard'lar sayesinde:
- Kullanıcı yetkilendirmesi yapabilir
- Route geçişlerini kontrol edebilir
- Veri yükleme işlemlerini yönetebilir
- Form değişikliklerini kontrol edebilir
- Analitik veriler toplayabilir
Global Navigation Guards
Global guard'lar, tüm route geçişlerini etkiler. Üç tip global guard vardır:
1. beforeEach
Her route geçişinden önce çalışır. Authentication kontrolü gibi genel kontroller için idealdir.
// router/guards/auth.ts import { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' import { useAuthStore } from '@/stores/auth' export function setupAuthGuard(router: Router) { router.beforeEach(async ( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext ) => { const authStore = useAuthStore() const requiresAuth = to.meta.requiresAuth as boolean const isPublicRoute = to.meta.public as boolean // Sayfa yüklenirken auth durumunu kontrol et if (!authStore.isInitialized) { try { await authStore.initialize() } catch (error) { console.error('Auth initialization failed:', error) next('/error') return } } // Auth gerektiren route kontrolü if (requiresAuth && !authStore.isAuthenticated) { // Orijinal hedefi kaydet next({ path: '/login', query: { redirect: to.fullPath } }) return } // Giriş yapmış kullanıcı login sayfasına gitmeye çalışırsa if (isPublicRoute && authStore.isAuthenticated) { next('/dashboard') return } // Role bazlı kontrol if (to.meta.roles) { const requiredRoles = to.meta.roles as string[] if (!authStore.hasRoles(requiredRoles)) { next('/forbidden') return } } // Permission bazlı kontrol if (to.meta.permissions) { const requiredPermissions = to.meta.permissions as string[] if (!authStore.hasPermissions(requiredPermissions)) { next('/forbidden') return } } next() }) }
2. beforeResolve
Route bileşenleri yüklendikten sonra, ama route geçişi tamamlanmadan önce çalışır.
// router/guards/dataLoader.ts router.beforeResolve(async (to) => { try { // Route'a özgü veri yükleme işlemleri const dataPromises = to.matched.map(async (record) => { if (record.components?.default.loadData) { return record.components.default.loadData() } }) await Promise.all(dataPromises) } catch (error) { console.error('Data loading failed:', error) return false // Route geçişini engelle } })
3. afterEach
Route geçişi tamamlandıktan sonra çalışır. Analytics tracking için idealdir.
// router/guards/analytics.ts router.afterEach((to, from) => { // Sayfa görüntüleme analitiklerini gönder analytics.trackPageView({ path: to.fullPath, title: to.meta.title, referrer: from.fullPath }) // Sayfa başlığını güncelle document.title = `${to.meta.title} - MyApp` // Scroll pozisyonunu sıfırla window.scrollTo(0, 0) })
Route-Specific Guards
Belirli route'lar için özel kontroller yapmak istediğimizde kullanılır.
// router/routes/admin.ts import type { RouteRecordRaw } from 'vue-router' import { usePermissionStore } from '@/stores/permission' const adminRoutes: RouteRecordRaw[] = [ { path: '/admin', component: () => import('@/views/admin/AdminLayout.vue'), meta: { requiresAuth: true, roles: ['admin'], title: 'Admin Panel' }, beforeEnter: [validateAdminAccess, loadAdminData], children: [ { path: 'users', component: () => import('@/views/admin/UserManagement.vue'), meta: { permissions: ['manage_users'] }, beforeEnter: async (to, from, next) => { const permissionStore = usePermissionStore() // Permission kontrolü if (!permissionStore.can('manage_users')) { next({ name: 'admin-dashboard' }) return } // Kullanıcı listesini ön yükle try { await permissionStore.loadUserPermissions() next() } catch (error) { next(false) } } }, { path: 'settings', component: () => import('@/views/admin/Settings.vue'), meta: { permissions: ['manage_settings'] }, // Multiple guard fonksiyonları beforeEnter: [checkSettingsAccess, loadSettingsData] } ] } ] // Guard helper fonksiyonları async function validateAdminAccess(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { const userRole = await getUserRole() if (userRole !== 'admin') { next({ path: '/forbidden', replace: true }) return } next() } async function loadAdminData(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { try { await Promise.all([ loadAdminStats(), loadRecentActivity() ]) next() } catch (error) { next(false) } } function checkSettingsAccess(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { const permissionStore = usePermissionStore() if (!permissionStore.can('manage_settings')) { next(false) return } next() } async function loadSettingsData(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { try { await loadSystemSettings() next() } catch (error) { next(false) } }
Component Guards
Component içinde kullanılan guard'lar, component'e özel logic'ler için idealdir.
<!-- views/ProductEdit.vue --> <script setup lang="ts"> import { ref, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' import type { Product } from '@/types' import { useProductStore } from '@/stores/product' import { useNotification } from '@/composables/useNotification' const productStore = useProductStore() const notification = useNotification() const hasUnsavedChanges = ref(false) const product = ref<Product | null>(null) const originalData = ref<Product | null>(null) // Form değişikliklerini takip et function handleFormChange() { if (!originalData.value || !product.value) return hasUnsavedChanges.value = !isEqual(originalData.value, product.value) } // Route değişmeden önce kontrol onBeforeRouteLeave((to, from, next) => { if (!hasUnsavedChanges.value) { next() return } // Custom dialog komponenti ile sor Dialog.confirm({ title: 'Kaydedilmemiş Değişiklikler', message: 'Kaydedilmemiş değişiklikleriniz var. Sayfadan ayrılmak istediğinize emin misiniz?', confirmText: 'Evet, Ayrıl', cancelText: 'Hayır, Devam Et', type: 'warning' }) .then(() => next()) .catch(() => next(false)) }) // Route params değiştiğinde ürünü güncelle onBeforeRouteUpdate(async (to, from, next) => { if (to.params.id === from.params.id) { next() return } if (hasUnsavedChanges.value) { const shouldProceed = await Dialog.confirm({ title: 'Kaydedilmemiş Değişiklikler', message: 'Değişikliklerinizi kaydetmeden başka bir ürüne geçmek istediğinize emin misiniz?' }) if (!shouldProceed) { next(false) return } } try { const newProduct = await productStore.fetchProduct(to.params.id as string) product.value = newProduct originalData.value = JSON.parse(JSON.stringify(newProduct)) hasUnsavedChanges.value = false next() } catch (error) { notification.error('Ürün yüklenirken bir hata oluştu') next(false) } }) // Ürünü kaydet async function saveProduct() { try { await productStore.updateProduct(product.value!) originalData.value = JSON.parse(JSON.stringify(product.value)) hasUnsavedChanges.value = false notification.success('Ürün başarıyla kaydedildi') } catch (error) { notification.error('Ürün kaydedilirken bir hata oluştu') } } </script> <template> <div v-if="product"> <form @change="handleFormChange"> <!-- Form içeriği --> <div class="form-group"> <label>Ürün Adı</label> <input v-model="product.name" type="text" /> </div> <div class="form-group"> <label>Fiyat</label> <input v-model="product.price" type="number" /> </div> <div class="form-actions"> <button type="button" @click="saveProduct" :disabled="!hasUnsavedChanges" > Kaydet </button> </div> </form> </div> </template>
Route Meta Fields ile Zengin Metadata
Route meta fields, route'lar hakkında ek bilgiler saklamak için kullanışlıdır.
// types/router.ts import 'vue-router' declare module 'vue-router' { interface RouteMeta { requiresAuth: boolean roles?: string[] permissions?: string[] layout?: 'default' | 'admin' | 'auth' | 'blank' title: string description?: string breadcrumb?: { label: string parent?: string } menu?: { icon?: string order?: number group?: string hideChildren?: boolean } } } // router/routes.ts const routes: RouteRecordRaw[] = [ { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true, roles: ['user', 'admin'], layout: 'default', title: 'Dashboard', description: 'User dashboard and analytics', breadcrumb: { label: 'Dashboard' }, menu: { icon: 'dashboard', order: 1 } } } ]
Middleware Pattern
Express.js benzeri bir middleware pattern'i implement edebiliriz:
// router/middleware/types.ts type Middleware = ( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext ) => Promise<void> | void // router/middleware/index.ts export function defineMiddleware(middleware: Middleware): Middleware { return middleware } // Örnek middleware'ler export const loggerMiddleware = defineMiddleware((to, from, next) => { console.log(`Navigating from ${from.path} to ${to.path}`) next() }) export const analyticsMiddleware = defineMiddleware((to, from, next) => { // Sayfa görüntüleme eventi gönder analytics.pageView({ page_path: to.fullPath, page_title: to.meta.title }) next() }) export const loadingMiddleware = defineMiddleware((to, from, next) => { // Global loading state'i yönet const loading = useLoadingStore() loading.start() next() // Route geçişi tamamlandığında loading'i kapat router.afterEach(() => { loading.finish() }) }) // Middleware'leri birleştir export function composeMiddleware(middlewares: Middleware[]) { return async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => { for (const middleware of middlewares) { try { await middleware(to, from, next) } catch (error) { console.error('Middleware error:', error) next(false) return } } next() } } // router/index.ts const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach( composeMiddleware([ loggerMiddleware, analyticsMiddleware, loadingMiddleware, authGuard ]) )
Error Handling
Navigation guard'larda hata yönetimi önemlidir:
// router/guards/errorHandler.ts router.onError((error) => { console.error('Router error:', error) // Kritik hataları raporla errorReporting.captureException(error) // Kullanıcıya bilgi ver notification.error('Bir hata oluştu. Lütfen sayfayı yenileyin.') // Hata sayfasına yönlendir router.push('/error') }) // Global error boundary app.config.errorHandler = (error, instance, info) => { console.error('Global error:', error) // Route hatalarını yakala if (error.name === 'NavigationFailure') { notification.warning('Sayfa yüklenemedi. Lütfen tekrar deneyin.') return } // Diğer hataları raporla errorReporting.captureException(error, { extra: { componentName: instance?.$options.name, errorInfo: info } }) }
Best Practices ve İpuçları
Guard'ları Modüler Tutun
- Her guard'ın tek bir sorumluluğu olsun
- Guard'ları ayrı dosyalarda tutun
- Tekrar kullanılabilir guard'lar oluşturun
Performans Optimizasyonu
- Gereksiz API çağrılarından kaçının
- Cache mekanizmaları kullanın
- Guard'larda async işlemleri optimize edin
Error Handling
- Guard'larda try-catch blokları kullanın
- Kullanıcıya anlamlı hata mesajları gösterin
- Hataları loglayın
TypeScript Kullanımı
- Route meta tiplerini tanımlayın
- Guard parametrelerini tiplendirin
- Custom tipleri modüler tutun
Sonuç
Vue Router'ın navigation guard'ları, modern web uygulamalarında route yönetimi için güçlü ve esnek bir çözüm sunar. Bu yazıda öğrendiğimiz pattern'leri kullanarak:
- Güvenli route geçişleri
- Etkili authentication/authorization
- Modüler ve maintainable kod
- İyi bir kullanıcı deneyimi
elde edebilirsiniz.
İlgili Etiketler: #VueRouter #NavigationGuards #Authentication #TypeScript #Middleware #WebDevelopment