Vue Router ile Navigation Guards ve Route Yönetimi

Yunus Emre Güzel
8 Ocak 202515 dkVue.js
Vue Router ile Navigation Guards ve Route Yönetimi

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ı

  1. 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
  2. Performans Optimizasyonu

    • Gereksiz API çağrılarından kaçının
    • Cache mekanizmaları kullanın
    • Guard'larda async işlemleri optimize edin
  3. Error Handling

    • Guard'larda try-catch blokları kullanın
    • Kullanıcıya anlamlı hata mesajları gösterin
    • Hataları loglayın
  4. 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

Kaynaklar