Vue router: маршрутизація та охоронці навігації
Vue Router відображає URL-шляхи на компоненти Vue і керує навігацією через охоронці (guards) - код, що запускається до, під час або після переходу між маршрутами.
Теорія
TL;DR
- Vue Router = адміністратор готелю: URL це номер кімнати, компонент це сама кімната, охоронець перевіряє перепустку на вході
- Порядок виконання:
beforeEach(глобальний) →beforeEnter(для маршруту) → завантаження async-компонентів →beforeResolve(глобальний) → навігація підтверджується →afterEach beforeEachдля перевірки auth і редиректів,beforeResolveдля prefetch даних,afterEachдля аналітики- Маршрут
/:pathMatch(.*)*завжди в кінці масиву - інакше він перехопить усі URL раніше за специфічні маршрути
Швидкий приклад
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./Home.vue') },
{
path: '/dashboard',
component: () => import('./Dashboard.vue'),
meta: { requiresAuth: true } // зчитується в охоронці нижче
},
{ path: '/:pathMatch(.*)*', component: () => import('./NotFound.vue') } // wildcard завжди останній
]
})
router.beforeEach((to) => {
// Запускається перед кожною навігацією
if (to.meta.requiresAuth && !localStorage.getItem('token')) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
export default routerПерехід на /dashboard без токена редиректить на /login?redirect=/dashboard. З токеном у localStorage компонент Dashboard завантажується звичайно.
Охоронці vs базовий routing
Без охоронців Vue Router пасивний: URL змінився, компонент підмінився, і все. Охоронці перетворюють це на активне управління потоком. Вони ставлять навігацію на паузу, запускають твій код і продовжують лише якщо ти дозволяєш.
Це важливо коли в застосунку є авторизація, права доступу або дані, які треба завантажити до рендеру сторінки. Базовий routing цього не вміє. Охоронці вміють.
Три типи охоронців
Глобальні охоронці прив'язані до екземпляра router і запускаються для кожної навігації в застосунку:
router.beforeEach((to) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return '/login'
}
})
router.afterEach((to) => {
analytics.track(to.fullPath) // після кожної успішної навігації
})Охоронці маршруту (beforeEnter) задаються прямо в конфігурації маршруту. Зручно, коли логіка стосується одного маршруту і не повинна засмічувати глобальний beforeEach:
const routes = [
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
beforeEnter: async (to, from) => {
const user = await fetchUser()
if (!user.isAdmin) return '/unauthorized'
}
}
]Охоронці в компоненті використовують composables всередині самого компонента. Головний сценарій - не дати користувачу піти зі сторінки з незбереженою формою:
<script setup>
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return window.confirm('Вийти без збереження?')
}
})
</script>Порядок виконання охоронців
Ось послідовність кроків, яку Vue Router виконує під час кожної навігації:
router.beforeEach(глобальний)beforeEnterна цільовому маршруті- Завантаження async-компонентів (dynamic imports запускаються тут)
router.beforeResolve(глобальний, після завантаження компонентів)- Навігація підтверджується, URL змінюється
router.afterEach(глобальний, після завершення навігації)
beforeEach запускається до завантаження будь-якого компонента, тому він дешевий для редиректів. beforeResolve краще підходить для запитів до API - до цього моменту компонент точно завантажений. Бачив команди, що клали важкі API-запити в beforeEach і дивувалися чому компоненти встигають мигнути до редиректу. Саме тому.
Meta-поля маршрутів
Meta-поля дозволяють прикріпити довільні дані до маршруту і зчитати їх в охоронцях:
const routes = [
{
path: '/profile/:id',
component: UserProfile,
meta: { requiresAuth: true, role: 'user' }
}
]
router.beforeEach((to) => {
if (to.meta.requiresAuth && !store.auth.isLoggedIn) {
return '/login?redirect=' + to.fullPath
}
})to.meta доступний в будь-якому охоронці, включно з глобальними. Meta-поля батьківського маршруту також доступні в дочірніх через to.matched.
Типові помилки
Подвійний виклик next(). Callback-стиль з параметром next досі підтримується, але часто збиває з пантелику. Якщо не повернути після виклику next, обидві гілки виконуються:
// Неправильно: обидві гілки виконуються коли умова хибна
router.beforeEach((to, from, next) => {
if (!auth) next('/login') // провалюється далі!
next()
})
// Правильно: ранній return
router.beforeEach((to, from, next) => {
if (!auth) return next('/login')
next()
})
// Краще: Vue Router 4 дозволяє просто повертати значення
router.beforeEach((to) => {
if (!auth) return '/login'
})Wildcard маршрут на неправильній позиції. /:pathMatch(.*)* відповідає будь-якому шляху. Якщо поставити його першим, жоден інший маршрут в масиві ніколи не спрацює:
// Неправильно: /users/:id ніколи не матчиться
routes: [
{ path: '/:pathMatch(.*)*', component: NotFound }, // перехоплює все
{ path: '/users/:id', component: UserProfile }
]
// Правильно: wildcard останній
routes: [
{ path: '/users/:id', component: UserProfile },
{ path: '/:pathMatch(.*)*', component: NotFound }
]afterEach логує зайві перегляди сторінок. Виклик next(false) скасовує навігацію, але afterEach все одно виконується. Аналітика зафіксує перегляд сторінки, на якій користувач так і не опинився. Перевіряй to.fullPath !== from.fullPath всередині afterEach, або встановлюй прапорець при скасуванні.
Запити до API в beforeEach замість beforeResolve. В момент виконання beforeEach async-компонент ще не завантажений. Запит стартує до підтвердження маршруту. Для даних, що прив'язані до компонента, використовуй beforeResolve.
Де використовується
- Nuxt 3 огортає охоронці у
defineNuxtRouteMiddlewareі сам розрізняє SSR і клієнт (на сервері читає cookies черезuseRequestHeaders(), на клієнті -localStorage) - Quasar Framework реєструє глобальні охоронці в
boot-файлах для перевірки PWA offline-стану перед навігацією - Pinia auth stores централізують логіку охоронців:
useAuthStore().check()вbeforeEachтримає перевірку токена в одному місці - VitePress використовує
beforeEachдля оновлення пошукового індексу під час навігації
Питання на співбесіді
Q: Який порядок виконання охоронців у Vue Router?
A: router.beforeEach → beforeEnter маршруту → завантаження async-компонентів → router.beforeResolve → підтвердження навігації → router.afterEach.
Q: Коли використовувати beforeResolve замість beforeEach?
A: beforeEach запускається до завантаження компонентів - підходить для редиректів та перевірки auth. beforeResolve запускається після завантаження компонентів, але до рендеру - саме там роблять prefetch даних.
Q: Як обробити async-операції в охоронці?
A: Поверни Promise з охоронця. Router чекатиме на його виконання. Якщо використовуєш next, викликай його тільки після завершення async-роботи.
Q: Як передати дані в охоронець через конфігурацію маршруту?
A: Додай будь-яке поле до об'єкта meta в конфігурації маршруту і зчитуй як to.meta.твоєПоле в будь-якому охоронці.
Q: (Senior) В Nuxt SSR як не допустити помилку hydration через охоронці що читають localStorage?
A: На сервері localStorage недоступний. Використовуй process.client щоб обмежити такий код тільки клієнтом, а для читання auth-cookies на сервері - useRequestHeaders(). Middleware в Nuxt 3 через defineNuxtRouteMiddleware обробляє цей поділ автоматично.
Q: (Senior) Користувач натискає "вийти" поки охоронець beforeEach виконує async-запит профілю. Що зламається і як виправити?
A: await виконається вже після logout, і стан auth буде застарілим - навігація може пройти. Рішення: AbortController для запиту і повторна перевірка authStore.isLoggedIn після await. Якщо false, одразу return '/login'.
Приклади
Охоронець авторизації зі збереженням цільового URL
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('@/views/Home.vue') },
{ path: '/login', component: () => import('@/views/Login.vue') },
{
path: '/profile/:id',
component: () => import('@/views/UserProfile.vue'),
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
// Зберігаємо цільовий шлях для редиректу після входу
return { path: '/login', query: { redirect: to.fullPath } }
}
})
export default routerПерехід на /profile/42 без авторизації редиректить на /login?redirect=/profile/42. Після успішного входу сторінка логіну читає route.query.redirect і відправляє користувача одразу на /profile/42.
Async перевірка ролі в охоронці маршруту
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
beforeEnter: async (to, from) => {
const user = await fetchUser() // API-запит тільки при переході на /dashboard
if (!user.isAdmin) return '/login'
}
}
]API-запит виконується лише при відвідуванні /dashboard. Не-адміни потрапляють на /login. Охоронець живе прямо в конфігурації маршруту, тому глобальний beforeEach залишається чистим.
Вкладені маршрути зі спільним layout
const routes = [
{
path: '/dashboard',
component: () => import('@/layouts/DashboardLayout.vue'),
meta: { requiresAuth: true }, // поширюється на всі дочірні маршрути
children: [
{ path: '', component: () => import('@/views/DashboardHome.vue') }, // /dashboard
{ path: 'settings', component: () => import('@/views/Settings.vue') }, // /dashboard/settings
{ path: 'profile', component: () => import('@/views/Profile.vue') } // /dashboard/profile
]
}
]<!-- DashboardLayout.vue -->
<template>
<div class="dashboard-layout">
<Sidebar />
<main>
<RouterView /> <!-- тут рендеряться дочірні компоненти -->
</main>
</div>
</template>meta: { requiresAuth: true } на батьківському маршруті поширюється на всіх дітей. Одна перевірка в beforeEach покриває весь розділ dashboard без повторення meta-поля на кожному дочірньому маршруті.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.