Урок 03

Динамические маршруты

Основы

[param] — динамический параметр в имени файла:

pages/users/[id].vue

URL: /users/1, /users/abc, /users/anything

Доступ к параметрам

<script setup lang="ts">
const route = useRoute();
const id = computed(() => route.params.id);
</script>

<template>
    <p>ID: {{ id }}</p>
</template>

Несколько параметров

pages/[lang]/blog/[slug].vue → /ru/blog/vue-intro

const lang = route.params.lang;   // 'ru'
const slug = route.params.slug;   // 'vue-intro'

Опциональные

[[id]] — двойные скобки:

pages/users/[[id]].vue
URLroute.params.id
/usersundefined
/users/5'5'

Catch-all

[...slug] — любое количество сегментов:

pages/[...slug].vue
URLroute.params.slug
/a['a']
/a/b/c['a', 'b', 'c']

Валидация параметров

definePageMeta с validate:

<script setup lang="ts">
definePageMeta({
    validate: (route) => {
        return /^\d+$/.test(route.params.id as string);
    },
});
</script>

Если false — редирект на 404.

Или редирект:

<script setup lang="ts">
definePageMeta({
    validate: (route) => {
        if (route.params.id === 'admin') {
            return { path: '/login' };
        }
        return true;
    },
});
</script>

useRoute vs useRouter

const route = useRoute();      // текущий маршрут
route.params.id;
route.query.q;

const router = useRouter();    // навигация
router.push('/');

Реактивность

route — реактивный.

const id = computed(() => route.params.id);

watch(() => route.params.id, (newId) => {
    loadUser(newId);
});

Переиспользование компонента

⚠️ Nuxt переиспользует компонент при смене параметров. onMounted не сработает.

Решение 1 — watch:

watch(
    () => route.params.id,
    (newId) => loadUser(newId)
);

Решение 2 — key:

<NuxtPage :key="route.fullPath" />

Решение 3 — через useFetch с реактивным URL:

const { data: user } = await useFetch(() => `/api/users/${route.params.id}`);

Nuxt сам перезапросит при смене URL.

Загрузка данных

useFetch — реактивный:

<script setup lang="ts">
const route = useRoute();

const { data: user, pending, error } = await useFetch(
    () => `/api/users/${route.params.id}`
);
</script>

<template>
    <p v-if="pending">Загрузка...</p>
    <p v-else-if="error">{{ error.message }}</p>
    <div v-else>
        <h1>{{ user?.name }}</h1>
    </div>
</template>

Что даёт:

  • SSR из коробки
  • Реактивность на изменение параметра
  • Кеш

useRoute с типизацией

const route = useRoute();
const id = route.params.id as string;

Или через generic (Nuxt 3.9+):

const route = useRoute<'/users/[id]'>();
const id = route.params.id;   // string

Практика — блог

pages/blog/[slug].vue:

<script setup lang="ts">
const route = useRoute();

const { data: post } = await useFetch(
    () => `/api/posts/${route.params.slug}`
);

if (!post.value) {
    throw createError({
        statusCode: 404,
        statusMessage: 'Post not found',
    });
}
</script>

<template>
    <article v-if="post">
        <h1>{{ post.title }}</h1>
        <div v-html="post.content" />
    </article>
</template>

Catch-all для 404

pages/[...slug].vue:

<script setup lang="ts">
const route = useRoute();

throw createError({
    statusCode: 404,
    statusMessage: 'Page not found',
    data: { path: route.fullPath },
});
</script>

Валидация нескольких параметров

definePageMeta({
    validate: (route) => {
        const { id, slug } = route.params;
        return /^\d+$/.test(id as string) && (slug as string).length > 0;
    },
});

Итог

  • [id] — динамика
  • [[id]] — опционально
  • [...slug] — catch-all
  • useRoute() — доступ
  • useFetch(() => ...) — реактивная загрузка
  • validate — проверка

Дальше — layouts.