29. Роутинг: Guards и Lazy Loading
🛡️ Продвинутый Vue Router
Заголовок раздела «🛡️ Продвинутый Vue Router»Базовую навигацию ты уже знаешь. Теперь — настоящая магия: защита маршрутов, ленивая загрузка, мета-данные, анимации переходов и именованные виды. Это то, что отличает junior-разработчика от профи 🚀
Navigation Guards — охранники маршрутов
Заголовок раздела «Navigation Guards — охранники маршрутов»Guards — это функции-перехватчики, которые могут разрешить, запретить или перенаправить навигацию. Думай о них как о middleware для роутов.
Global Guards — глобальные охранники
Заголовок раздела «Global Guards — глобальные охранники»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)})Per-Route Guards — охранники маршрута
Заголовок раздела «Per-Route Guards — охранники маршрута»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' } }}In-Component Guards
Заголовок раздела «In-Component Guards»<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>Route Meta — метаданные маршрутов
Заголовок раздела «Route Meta — метаданные маршрутов»// Расширяем типы для metaimport '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' }}
// Использование в routesconst 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()
// Хлебные крошки из metaconst breadcrumbs = computed(() => route.meta.breadcrumb || [])</script>Lazy Loading — ленивая загрузка
Заголовок раздела «Lazy Loading — ленивая загрузка»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') }})Scroll Behavior — поведение скролла
Заголовок раздела «Scroll Behavior — поведение скролла»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', } },})Named Views — несколько RouterView
Заголовок раздела «Named Views — несколько RouterView»Когда нужно несколько независимых областей с разным контентом:
{ path: '/dashboard', components: { default: DashboardMain, // <RouterView /> header: DashboardHeader, // <RouterView name="header" /> sidebar: DashboardSidebar, // <RouterView name="sidebar" /> }}<template> <RouterView name="header" /> <div class="layout"> <RouterView name="sidebar" /> <main> <RouterView /> </main> </div></template>Transitions — анимации переходов
Заголовок раздела «Transitions — анимации переходов»<!-- 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()