Що таке middleware в Nuxt і як його використовувати?
Nuxt middleware - це функція, яка виконується до того як компонент сторінки монтується, дозволяючи перевіряти умови, перенаправляти користувачів або повністю блокувати навігацію.
Теорія
TL;DR
- Аналогія: охорона в аеропорту - перевіряє квиток (роут), пропускає, відправляє на реєстрацію (login) або зупиняє
- Три типи: global (кожен роут), named (підключається окремо для кожної сторінки), inline (одноразовий у
definePageMeta) - Повертає
navigateTo()для редиректу абоabortNavigation()щоб зупинити без зміни URL - Для auth і route guards - так; для завантаження даних - ні (для цього є
useAsyncData) - Глобальні middleware виконуються в алфавітному порядку за назвою файлу, потім named, потім inline
Швидкий приклад
// 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:
// pages/admin/users.vue
definePageMeta({
middleware: 'admin' // запускає middleware/admin.ts
});Inline middleware - це функція прямо в definePageMeta. Підходить для одноразових перевірок, які не потрібно повторно використовувати:
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
// неправильно
to.path = '/login'; // нічого не відбудетьсяto - це readonly об'єкт RouteLocation. Vue Router ігнорує будь-які прямі мутації. Потрібно return navigateTo('/login').
2. localStorage для перевірки авторизації
// неправильно
const token = localStorage.getItem('token'); // undefined на серверіSSR не має localStorage. Middleware виконується на сервері, не знаходить токен і робить редирект. Потім на клієнті знаходить токен і пропускає. Поведінка непередбачувана, гідратація ламається. Використовуй useCookie() або useState() - вони однаково працюють на сервері і клієнті.
3. Втрата redirect-history
// неправильно - після логіну нема куди повернутись
return navigateTo('/login');
// правильно
return navigateTo({ path: '/login', query: { redirect: from.path } });Передавай from.path як query-параметр. Після логіну прочитай його і відправ користувача туди, куди він хотів потрапити.
4. Глобальний middleware блокує Nuxt DevTools
Глобальна перевірка авторизації перехопить і внутрішні шляхи Nuxt на зразок /__nuxt_devtools__. Додай ранній return:
if (to.path.startsWith('/__')) return; // пропускаємо внутрішні шляхи5. Async без await
// неправильно - ланцюжок продовжується до 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через composableuseAuth() - 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
// 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-дашборді
// 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-операціями
// 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 при швидких переходах.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.