Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке middleware в Nuxt і як його використовувати?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Nuxt middleware** - це функція, яка виконується до монтування сторінки. Використовується для авторизації, редиректів і контролю доступу. Типи: global (кожен роут, суфікс `.global.ts`), named (через `definePageMeta`), inline (функція в `definePageMeta`). Повертає `navigateTo()` або `abortNavigation()`. **Головне:** виконується до компонента, тому може повністю заблокувати рендеринг.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Nuxt middleware** - це функція, яка виконується до того як компонент сторінки монтується, дозволяючи перевіряти умови, перенаправляти користувачів або повністю блокувати навігацію. ## Теорія ### TL;DR - Аналогія: охорона в аеропорту - перевіряє квиток (роут), пропускає, відправляє на реєстрацію (login) або зупиняє - Три типи: global (кожен роут), named (підключається окремо для кожної сторінки), inline (одноразовий у `definePageMeta`) - Повертає `navigateTo()` для редиректу або `abortNavigation()` щоб зупинити без зміни URL - Для auth і route guards - так; для завантаження даних - ні (для цього є `useAsyncData`) - Глобальні middleware виконуються в алфавітному порядку за назвою файлу, потім named, потім inline ### Швидкий приклад ```ts // middleware/auth.global.ts export default defineNuxtRouteMiddleware((to, from) => { const token = useCookie('auth').value; if (!token && to.path !== '/login') { return navigateTo('/login'); // перенаправляємо неавторизованих } // без return = продовжуємо навігацію }); ``` Неавторизований користувач на `/dashboard` автоматично потрапляє на `/login`. Авторизований проходить далі. Суфікс `.global.ts` означає, що цей middleware спрацює на кожному роуті без будь-якої додаткової конфігурації. ### Головна відмінність від Vue composables Middleware перехоплює навігацію на рівні роутера, до того як компонент сторінки монтується. `useFetch` і `onMounted` виконуються вже після. Саме цей проміжок дозволяє middleware повністю заблокувати рендеринг і уникнути "спалаху приватного контенту". Якщо покласти перевірку авторизації в `onMounted`, сторінка спочатку відрендериться, а потім відбудеться редирект. Я бачив, як команди дебажили цей "мерехтливий дашборд" годинами, не підозрюючи що причина в порядку виконання. Поганий UX і сумнівна безпека. ### Типи middleware **Global middleware** розташовується у папці `middleware/` із суфіксом `.global.ts`. Nuxt підхоплює його автоматично і запускає на кожній зміні роуту. Між глобальними middleware порядок визначається алфавітно за назвою файлу. **Named middleware** теж живе у `middleware/`, але без суфікса. Підключається до конкретних сторінок через `definePageMeta`: ```ts // pages/admin/users.vue definePageMeta({ middleware: 'admin' // запускає middleware/admin.ts }); ``` **Inline middleware** - це функція прямо в `definePageMeta`. Підходить для одноразових перевірок, які не потрібно повторно використовувати: ```ts definePageMeta({ middleware: (to, from) => { if (!hasFeatureFlag('beta')) { return navigateTo('/'); } } }); ``` ### Коли використовувати - Перевірка авторизації та сесій: global або named middleware з `useCookie()` або Pinia store - Доступ за ролями (сторінки тільки для адміна): named middleware через `definePageMeta` - Одноразові feature flags або A/B тести: inline middleware - SEO-редиректи (канонічні URL, застарілі шляхи): global middleware Для завантаження даних middleware не підходить - для цього є `useAsyncData` та `useFetch`. UI-логіка теж сюди не належить. Вона живе в `onMounted` або composables. ### Як це працює всередині Коли змінюється роут, Nuxt збирає всі middleware функції по порядку: спочатку глобальні (за алфавітом назви файлу), потім named зі сторінки, потім inline. Вони виконуються по черзі. Перша функція, яка повертає щось відмінне від `null` або `undefined`, зупиняє ланцюжок. На сервері (SSR) Nitro запускає ті самі middleware перед `render:page`. На клієнті це робить хук `beforeEach` Vue Router. При першому завантаженні middleware виконується двічі: на сервері і під час гідратації на клієнті. Саме тому `localStorage` не працює в middleware - на сервері його не існує. ### Типові помилки **1. Пряма зміна `to.path`** ```ts // неправильно to.path = '/login'; // нічого не відбудеться ``` `to` - це readonly об'єкт `RouteLocation`. Vue Router ігнорує будь-які прямі мутації. Потрібно `return navigateTo('/login')`. **2. `localStorage` для перевірки авторизації** ```ts // неправильно const token = localStorage.getItem('token'); // undefined на сервері ``` SSR не має `localStorage`. Middleware виконується на сервері, не знаходить токен і робить редирект. Потім на клієнті знаходить токен і пропускає. Поведінка непередбачувана, гідратація ламається. Використовуй `useCookie()` або `useState()` - вони однаково працюють на сервері і клієнті. **3. Втрата redirect-history** ```ts // неправильно - після логіну нема куди повернутись return navigateTo('/login'); // правильно return navigateTo({ path: '/login', query: { redirect: from.path } }); ``` Передавай `from.path` як query-параметр. Після логіну прочитай його і відправ користувача туди, куди він хотів потрапити. **4. Глобальний middleware блокує Nuxt DevTools** Глобальна перевірка авторизації перехопить і внутрішні шляхи Nuxt на зразок `/__nuxt_devtools__`. Додай ранній return: ```ts if (to.path.startsWith('/__')) return; // пропускаємо внутрішні шляхи ``` **5. Async без await** ```ts // неправильно - ланцюжок продовжується до resolve fetch('/api/user').then(user => { /* ... */ }); // правильно const user = await $fetch('/api/user'); if (!user) return navigateTo('/login'); ``` Без `await` middleware повертає `undefined` одразу і наступна функція в ланцюжку запускається до завершення асинхронної перевірки. ### Де використовується - Модуль `nuxt-auth`: перевірка сесії в `auth.global.ts` через composable `useAuth()` - A/B тести: named middleware призначає користувача до варіанту і редиректить на відповідну версію сторінки - Stripe checkout: inline middleware перевіряє сесію перед показом сторінки оплати - Редиректи зі старих URL: global middleware маппить застарілі шляхи на нові до рендерингу будь-якої сторінки ### Питання на співбесіді **Q:** В чому різниця між `navigateTo()` і `abortNavigation()`? **A:** `navigateTo()` змінює URL і рендерить іншу сторінку. `abortNavigation()` зупиняє поточну навігацію без зміни URL. Можна передати об'єкт помилки в `abortNavigation()`, щоб показати сторінку помилки Nuxt із власним повідомленням. **Q:** В якому порядку виконуються middleware? **A:** Спочатку глобальні - в алфавітному порядку за назвою файлу. Потім named у порядку, вказаному в `definePageMeta`. Потім inline. Ланцюжок зупиняється на першому ненульовому значенні, що повертається. **Q:** Чому мій middleware виконується двічі при першому завантаженні? **A:** З SSR middleware виконується на сервері і повторно на клієнті під час гідратації. При подальших клієнтських переходах - тільки в браузері. Використовуй `process.server` або `process.client` якщо потрібно обмежити виконання одним середовищем. **Q:** Чи може middleware отримати доступ до даних з `useAsyncData`? **A:** Ні. Middleware виконується до того як `useAsyncData` резолвиться. Для спільних даних використовуй `useState()` або Pinia store, який заповнюється в server plugin або раніше в ланцюжку запиту. **Q:** Як побудувати cache-aware middleware, який пропускає повторні перевірки авторизації для нещодавніх відвідувань? **A:** Зберігай короткоживучий cookie після успішної валідації: `useCookie('lastCheck', { maxAge: 300 })`. При наступних запитах, якщо цей cookie існує і session cookie теж є, пропускай API-запит і повертай одразу. Обидва cookies синхронізуються між сервером і клієнтом, тому SSR залишається стабільним. Цей підхід рятує на роутах з великим трафіком, де звертання до auth API на кожен перехід надто дороге. ## Приклади ### Базовий: глобальний auth guard ```ts // middleware/auth.global.ts export default defineNuxtRouteMiddleware((to, from) => { const token = useCookie('auth').value; if (!token && to.path !== '/login') { // зберігаємо цільову сторінку для редиректу після логіну return navigateTo({ path: '/login', query: { redirect: to.path } }); } }); ``` Кожна захищена сторінка перенаправляє неавторизованих на `/login?redirect=/dashboard`. Після логіну читаємо `route.query.redirect` і відправляємо туди куди йшли. Без query-параметра користувач завжди потрапляє на головну, навіть якщо хотів на конкретну сторінку. ### Середній: доступ за ролями в SaaS-дашборді ```ts // middleware/admin.ts export default defineNuxtRouteMiddleware((to) => { const { user } = useUserStore(); // Pinia store if (!user.loggedIn) { return navigateTo('/login'); } if (user.role !== 'admin' && to.path.startsWith('/admin')) { abortNavigation('Admin access required'); // рендерить сторінку помилки } }); // pages/admin/users.vue definePageMeta({ middleware: 'admin' }); ``` Адмін отримує сторінку. Залогінений не-адмін бачить сторінку помилки. Гість іде на `/login`. Три сценарії, один файл middleware, нуль дублювання перевірок між admin-сторінками. ### Просунутий: ланцюжок глобальних middleware з async-операціями ```ts // middleware/auth.global.ts (запускається першим - 'a' < 'l' за алфавітом) export default defineNuxtRouteMiddleware((to) => { const token = useCookie('token').value; if (!token) { // рендерить сторінку 401 без зміни URL, без спалаху контенту throw createError({ statusCode: 401, message: 'Unauthorized' }); } }); // middleware/logger.global.ts (запускається тільки якщо auth пропустив) export default defineNuxtRouteMiddleware(async (to) => { await $fetch('/api/log', { method: 'POST', body: { path: to.path } }); }); ``` Запит без токена: auth кидає 401, logger не виконується взагалі. З токеном: auth пропускає, logger записує відвідування. `await` у logger затримує рендеринг до завершення запиту. Якщо це критично для продуктивності, можна прибрати `await`, але тоді деякі записи можуть не потрапити до API при швидких переходах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.