Перейти к содержимому

29. Роутинг: Guards и Lazy Loading

Базовую навигацию ты уже знаешь. Теперь — настоящая магия: защита маршрутов, ленивая загрузка, мета-данные, анимации переходов и именованные виды. Это то, что отличает junior-разработчика от профи 🚀


Guards — это функции-перехватчики, которые могут разрешить, запретить или перенаправить навигацию. Думай о них как о middleware для роутов.

router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({ /* ... */ })
// beforeEach — выполняется ПЕРЕД каждой навигацией
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// to — куда идём
// from — откуда идём
// next — функция для подтверждения навигации
// Ждём инициализации авторизации
if (!authStore.isInitialized) {
await authStore.initialize()
}
// Проверяем, требует ли маршрут авторизации
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
// Редиректим на логин, запоминая куда хотели попасть
next({
name: 'login',
query: { redirect: to.fullPath }
})
return
}
// Гость пытается зайти на страницу только для авторизованных
if (to.meta.guestOnly && authStore.isLoggedIn) {
next({ name: 'dashboard' })
return
}
// Проверяем роль пользователя
if (to.meta.role && authStore.user?.role !== to.meta.role) {
next({ name: 'forbidden' })
return
}
// Всё хорошо — продолжаем
next()
})
// afterEach — выполняется ПОСЛЕ каждой навигации (без next)
router.afterEach((to, from) => {
// Аналитика
gtag('config', 'GA-ID', { page_path: to.path })
// Обновляем заголовок страницы
document.title = to.meta.title
? \`\${to.meta.title} | Мой сайт\`
: 'Мой сайт'
})
// Глобальная обработка ошибок навигации
router.onError((error) => {
console.error('Router error:', error)
})
const routes = [
{
path: '/admin',
component: AdminView,
// beforeEnter — выполняется только для этого маршрута
beforeEnter: (to, from) => {
const auth = useAuthStore()
if (!auth.isAdmin) {
return { name: 'forbidden' }
// Можно вернуть false для отмены навигации
// Или объект маршрута для редиректа
}
// undefined или true — продолжаем
},
},
{
path: '/settings',
component: SettingsView,
// Массив guards
beforeEnter: [checkAuth, checkEmailVerified, logAccess],
},
]
// Guards как отдельные функции
function checkAuth(to: RouteLocationNormalized) {
if (!isLoggedIn()) {
return { name: 'login', query: { redirect: to.fullPath } }
}
}
function checkEmailVerified() {
if (!currentUser()?.emailVerified) {
return { name: 'verify-email' }
}
}
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const hasUnsavedChanges = ref(false)
// Перед уходом со страницы
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const confirmed = confirm('Есть несохранённые изменения. Уйти?')
if (!confirmed) {
return false // Отменяем навигацию
}
}
})
// При обновлении маршрута (изменение params без смены компонента)
// Например: /users/1 → /users/2 — компонент тот же, params меняются
onBeforeRouteUpdate(async (to) => {
// Загружаем новые данные для нового пользователя
await fetchUser(to.params.id as string)
})
</script>

// Расширяем типы для meta
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
guestOnly?: boolean
role?: 'admin' | 'moderator'
title?: string
breadcrumb?: string[]
transition?: string
layout?: 'default' | 'auth' | 'admin'
}
}
// Использование в routes
const routes = [
{
path: '/',
component: HomeView,
meta: {
title: 'Главная',
breadcrumb: ['Главная'],
},
},
{
path: '/admin',
component: AdminView,
meta: {
requiresAuth: true,
role: 'admin',
title: 'Панель администратора',
layout: 'admin',
transition: 'slide-left',
},
},
{
path: '/login',
component: LoginView,
meta: {
guestOnly: true, // Только для неавторизованных
title: 'Вход',
layout: 'auth',
},
},
]
<!-- Использование meta в компоненте -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
// Хлебные крошки из meta
const breadcrumbs = computed(() => route.meta.breadcrumb || [])
</script>

const routes = [
// 1. Базовая ленивая загрузка
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
},
// 2. Именованный чанк (для группировки)
{
path: '/admin',
component: () => import(/* webpackChunkName: "admin" */ './views/Admin.vue'),
},
// 3. С обработкой ошибок — defineAsyncComponent
{
path: '/heavy',
component: defineAsyncComponent({
loader: () => import('./views/HeavyView.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200, // Показываем loading через 200мс
timeout: 10000, // Ошибка через 10 секунд
}),
},
]
// Prefetching — предзагрузка следующих маршрутов
router.beforeEach((to) => {
// При переходе на главную, предзагружаем дашборд
if (to.name === 'home') {
import('./views/Dashboard.vue')
}
})

const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// Возвращаем позицию скролла при нажатии "Назад"
if (savedPosition) {
return savedPosition
}
// Якорные ссылки (#section)
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth',
top: 80, // Отступ для фиксированного хедера
}
}
// Плавный скролл наверх при каждой навигации
return {
top: 0,
behavior: 'smooth',
}
},
})

Когда нужно несколько независимых областей с разным контентом:

router/index.ts
{
path: '/dashboard',
components: {
default: DashboardMain, // <RouterView />
header: DashboardHeader, // <RouterView name="header" />
sidebar: DashboardSidebar, // <RouterView name="sidebar" />
}
}
App.vue
<template>
<RouterView name="header" />
<div class="layout">
<RouterView name="sidebar" />
<main>
<RouterView />
</main>
</div>
</template>

<!-- App.vue — анимации при смене маршрутов -->
<template>
<RouterView v-slot="{ Component, route }">
<Transition
:name="route.meta.transition || 'fade'"
mode="out-in"
>
<component :is="Component" :key="route.path" />
</Transition>
</RouterView>
</template>
<style>
/* Fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Slide left */
.slide-left-enter-active,
.slide-left-leave-active {
transition: all 0.3s ease;
}
.slide-left-enter-from {
opacity: 0;
transform: translateX(30px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-30px);
}
</style>

// Добавление маршрутов в рантайме
router.addRoute({
name: 'plugin-page',
path: '/plugin',
component: PluginComponent,
})
// Добавление дочернего маршрута
router.addRoute('admin', {
path: 'users',
component: AdminUsers,
})
// Удаление маршрута
const removeRoute = router.addRoute(routeConfig)
removeRoute() // Удаляет добавленный маршрут
// Проверка существования маршрута
if (router.hasRoute('admin')) {
console.log('Маршрут admin существует')
}
// Получение всех маршрутов
const allRoutes = router.getRoutes()