Урок 05

Middleware

Что такое

Middleware — код, который выполняется до рендера страницы. Проверка авторизации, редиректы, логирование.

Типы

  • Anonymous — inline в странице
  • Named — файл в middleware/
  • Global — для всех страниц

Anonymous middleware

<script setup lang="ts">
definePageMeta({
    middleware: [
        (to, from) => {
            if (!isLoggedIn()) {
                return navigateTo('/login');
            }
        },
    ],
});
</script>

Только для этой страницы.

Named middleware

middleware/auth.ts:

export default defineNuxtRouteMiddleware((to, from) => {
    const { isLoggedIn } = useAuth();

    if (!isLoggedIn.value) {
        return navigateTo({
            path: '/login',
            query: { redirect: to.fullPath },
        });
    }
});

Использование:

<script setup lang="ts">
definePageMeta({
    middleware: 'auth',
});
</script>

Global middleware

middleware/analytics.global.ts — суффикс .global:

export default defineNuxtRouteMiddleware((to) => {
    if (import.meta.client) {
        // Трекинг на клиенте
        trackPageView(to.fullPath);
    }
});

Применяется ко всем страницам.

Порядок выполнения

  1. Global middleware (по алфавиту)
  2. Named middleware страницы
  3. Anonymous middleware

Параметры

export default defineNuxtRouteMiddleware((to, from) => {
    // to — куда
    // from — откуда
    // to.path, to.params, to.query
});

Возвраты

undefined — продолжить переход.

false — отменить.

navigateTo('/path') — редирект.

abortNavigation() — отменить с ошибкой.

Пример:

export default defineNuxtRouteMiddleware((to) => {
    if (to.path === '/admin' && !isAdmin()) {
        return navigateTo('/');
    }
    // продолжить
});

Async middleware

export default defineNuxtRouteMiddleware(async (to) => {
    const user = await fetchUser();

    if (!user && to.meta.requiresAuth) {
        return navigateTo('/login');
    }
});

Middleware с useAuth

composables/useAuth.ts:

export function useAuth() {
    const user = useState<User | null>('auth-user', () => null);
    const isLoggedIn = computed(() => !!user.value);

    async function login(credentials: LoginData) {
        const data = await $fetch<AuthResponse>('/api/login', {
            method: 'POST',
            body: credentials,
        });
        user.value = data.user;
        return data;
    }

    async function logout() {
        await $fetch('/api/logout', { method: 'POST' });
        user.value = null;
    }

    return { user, isLoggedIn, login, logout };
}

middleware/auth.ts:

export default defineNuxtRouteMiddleware((to) => {
    const { isLoggedIn } = useAuth();

    if (!isLoggedIn.value) {
        return navigateTo(`/login?redirect=${to.fullPath}`);
    }
});

middleware/guest.ts — для незалогиненных:

export default defineNuxtRouteMiddleware(() => {
    const { isLoggedIn } = useAuth();

    if (isLoggedIn.value) {
        return navigateTo('/dashboard');
    }
});

Meta-теги в страницах

pages/admin.vue:

<script setup lang="ts">
definePageMeta({
    middleware: 'auth',
    requiresAuth: true,
});
</script>

Тип для meta:

types/nuxt.d.ts:

declare module '#app' {
    interface PageMeta {
        requiresAuth?: boolean;
        requiresAdmin?: boolean;
    }
}

Middleware + meta

export default defineNuxtRouteMiddleware((to) => {
    const { isLoggedIn, isAdmin } = useAuth();

    if (to.meta.requiresAuth && !isLoggedIn.value) {
        return navigateTo('/login');
    }

    if (to.meta.requiresAdmin && !isAdmin.value) {
        return navigateTo('/');
    }
});

navigateTo

navigateTo('/about');
navigateTo({ path: '/about', query: { from: 'home' } });
navigateTo({ name: 'user', params: { id: 5 } });
navigateTo('/login', { external: true });   // внешняя ссылка

В middleware возвращай navigateTo(...).

abortNavigation

abortNavigation();
// или с ошибкой
abortNavigation(createError({
    statusCode: 403,
    statusMessage: 'Forbidden',
}));

Client vs Server

export default defineNuxtRouteMiddleware((to) => {
    if (import.meta.server) {
        // только на сервере
    }

    if (import.meta.client) {
        // только на клиенте
    }
});

Практика — защита админки

middleware/admin.ts:

export default defineNuxtRouteMiddleware(async () => {
    const { user, isLoggedIn } = useAuth();

    if (!isLoggedIn.value) {
        return navigateTo('/login');
    }

    // На сервере — проверить через API
    if (import.meta.server) {
        try {
            const data = await $fetch('/api/me');
            user.value = data;
        } catch {
            return navigateTo('/login');
        }
    }

    if (!user.value?.isAdmin) {
        return navigateTo('/');
    }
});

pages/admin.vue:

<script setup lang="ts">
definePageMeta({
    middleware: 'admin',
});
</script>

Несколько middleware

<script setup lang="ts">
definePageMeta({
    middleware: ['auth', 'verified', 'admin'],
});
</script>

Выполняются по порядку.

Логирование

middleware/logger.global.ts:

export default defineNuxtRouteMiddleware((to) => {
    if (import.meta.client) {
        console.log(`Navigate: ${to.path}`);
    }
});

Итог

  • Anonymous — inline в definePageMeta
  • Named — файл в middleware/
  • Global — .global.ts
  • navigateTo — редирект
  • abortNavigation — отмена
  • Порядок: global → named → anonymous

Дальше — состояние в Nuxt.