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