Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Vue router: маршрутизація та охоронці навігації». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Vue Router** відображає URL-шляхи на компоненти Vue і керує навігацією через охоронці (guards), що запускаються до, під час або після переходу між маршрутами. ```typescript router.beforeEach((to) => { if (to.meta.requiresAuth && !auth.isLoggedIn) { return { path: '/login', query: { redirect: to.fullPath } } } }) ``` **Головне:** порядок охоронців: `beforeEach` → `beforeEnter` → `beforeResolve` → `afterEach`. У Vue Router 4 замість `next()` достатньо повернути значення через `return`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Vue Router** відображає URL-шляхи на компоненти Vue і керує навігацією через охоронці (guards) - код, що запускається до, під час або після переходу між маршрутами. ## Теорія ### TL;DR - Vue Router = адміністратор готелю: URL це номер кімнати, компонент це сама кімната, охоронець перевіряє перепустку на вході - Порядок виконання: `beforeEach` (глобальний) → `beforeEnter` (для маршруту) → завантаження async-компонентів → `beforeResolve` (глобальний) → навігація підтверджується → `afterEach` - `beforeEach` для перевірки auth і редиректів, `beforeResolve` для prefetch даних, `afterEach` для аналітики - Маршрут `/:pathMatch(.*)*` завжди в кінці масиву - інакше він перехопить усі URL раніше за специфічні маршрути ### Швидкий приклад ```typescript // 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 і запускаються для кожної навігації в застосунку: ```typescript router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isLoggedIn) { return '/login' } }) router.afterEach((to) => { analytics.track(to.fullPath) // після кожної успішної навігації }) ``` **Охоронці маршруту** (`beforeEnter`) задаються прямо в конфігурації маршруту. Зручно, коли логіка стосується одного маршруту і не повинна засмічувати глобальний `beforeEach`: ```typescript const routes = [ { path: '/admin', component: () => import('@/views/Admin.vue'), beforeEnter: async (to, from) => { const user = await fetchUser() if (!user.isAdmin) return '/unauthorized' } } ] ``` **Охоронці в компоненті** використовують composables всередині самого компонента. Головний сценарій - не дати користувачу піти зі сторінки з незбереженою формою: ```vue <script setup> import { onBeforeRouteLeave } from 'vue-router' onBeforeRouteLeave((to, from) => { if (hasUnsavedChanges.value) { return window.confirm('Вийти без збереження?') } }) </script> ``` ### Порядок виконання охоронців Ось послідовність кроків, яку Vue Router виконує під час кожної навігації: 1. `router.beforeEach` (глобальний) 2. `beforeEnter` на цільовому маршруті 3. Завантаження async-компонентів (dynamic imports запускаються тут) 4. `router.beforeResolve` (глобальний, після завантаження компонентів) 5. Навігація підтверджується, URL змінюється 6. `router.afterEach` (глобальний, після завершення навігації) `beforeEach` запускається до завантаження будь-якого компонента, тому він дешевий для редиректів. `beforeResolve` краще підходить для запитів до API - до цього моменту компонент точно завантажений. Бачив команди, що клали важкі API-запити в `beforeEach` і дивувалися чому компоненти встигають мигнути до редиректу. Саме тому. ### Meta-поля маршрутів Meta-поля дозволяють прикріпити довільні дані до маршруту і зчитати їх в охоронцях: ```typescript 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`, обидві гілки виконуються: ```typescript // Неправильно: обидві гілки виконуються коли умова хибна 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(.*)*` відповідає будь-якому шляху. Якщо поставити його першим, жоден інший маршрут в масиві ніколи не спрацює: ```typescript // Неправильно: /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 ```typescript // 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 перевірка ролі в охоронці маршруту ```typescript 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 ```typescript 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 ] } ] ``` ```vue <!-- DashboardLayout.vue --> <template> <div class="dashboard-layout"> <Sidebar /> <main> <RouterView /> <!-- тут рендеряться дочірні компоненти --> </main> </div> </template> ``` `meta: { requiresAuth: true }` на батьківському маршруті поширюється на всіх дітей. Одна перевірка в `beforeEach` покриває весь розділ dashboard без повторення meta-поля на кожному дочірньому маршруті.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.