Nuxt генерирует маршруты автоматически из структуры папки pages/:
pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
├── blog/
│ ├── index.vue → /blog
│ └── [slug].vue → /blog/vue-3-tutorial
├── users/
│ ├── index.vue → /users
│ └── [id]/
│ ├── index.vue → /users/42
│ └── edit.vue → /users/42/edit
└── [...404].vue → /любой/несуществующий/путь<!-- pages/users/[id].vue -->
<script setup>
const route = useRoute()
const id = route.params.id // '42'
// Загрузка данных по параметру
const { data: user } = await useFetch(`/api/users/${id}`)
</script>
<template>
<div>
<h1>{{ user?.name }}</h1>
</div>
</template>pages/
└── users/
├── index.vue → /users (список)
└── [id].vue → /users/:id (детали)Вложенный layout через <NuxtPage>:
<!-- pages/users.vue — родительский layout -->
<template>
<div>
<Sidebar />
<NuxtPage /> <!-- дочерние маршруты рендерятся здесь -->
</div>
</template><!-- layouts/default.vue — применяется ко всем страницам -->
<template>
<div>
<Header />
<main>
<slot /> <!-- контент страницы -->
</main>
<Footer />
</div>
</template><!-- layouts/admin.vue — кастомный layout -->
<template>
<div class="admin">
<AdminSidebar />
<slot />
</div>
</template>// pages/admin/index.vue — использование кастомного layout
definePageMeta({ layout: 'admin' })// middleware/auth.ts — защита маршрутов
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value && to.path !== '/login') {
return navigateTo('/login') // редирект
}
})// Применение к конкретным страницам:
// pages/dashboard.vue
definePageMeta({
middleware: 'auth'
})
// Или к группе маршрутов:
definePageMeta({
middleware: ['auth', 'role-check']
})// Программная навигация
const router = useRouter()
const route = useRoute()
// Способ 1: navigateTo() (рекомендуется в Nuxt)
await navigateTo('/about')
await navigateTo({ name: 'users-id', params: { id: 42 } })
await navigateTo('https://example.com', { external: true })
// Способ 2: router.push()
router.push('/about')
router.replace('/about')
router.back()
// Чтение текущего маршрута
console.log(route.path) // '/users/42'
console.log(route.params.id) // '42'
console.log(route.query.page) // '2' (из ?page=2)Метаданные страницы (SSR, cache, layout, middleware):
definePageMeta({
layout: 'admin',
middleware: ['auth'],
ssr: false, // отключить SSR для этой страницы
title: 'Страница', // SEO заголовок
keepalive: true, // кэшировать компонент страницы
})Реализация файлового роутера — генерация маршрутов из дерева файлов и навигация с middleware
// Реализуем упрощённый файловый роутер Nuxt:
// генерируем маршруты из файловой структуры и обрабатываем навигацию.
// --- Генератор маршрутов из файлов ---
function buildRoutesFromFiles(files) {
return files.map(filePath => {
const withoutExt = filePath.replace(/\.vue$/, '')
const segments = withoutExt.split('/').map(seg => {
if (seg.startsWith('[...') && seg.endsWith(']'))
return { type: 'catchAll', param: seg.slice(4, -1) }
if (seg.startsWith('[[') && seg.endsWith(']]'))
return { type: 'optional', param: seg.slice(2, -2) }
if (seg.startsWith('[') && seg.endsWith(']'))
return { type: 'dynamic', param: seg.slice(1, -1) }
if (seg === 'index')
return { type: 'index' }
return { type: 'static', value: seg }
})
// Убираем финальный index
if (segments.at(-1)?.type === 'index') segments.pop()
const pathParts = segments.map(s => {
if (s.type === 'static') return s.value
if (s.type === 'dynamic') return ':' + s.param
if (s.type === 'optional') return ':' + s.param + '?'
if (s.type === 'catchAll') return ':' + s.param + '*'
return ''
}).filter(Boolean)
return {
path: '/' + pathParts.join('/'),
file: filePath,
params: segments.filter(s => s.type !== 'static' && s.type !== 'index').map(s => s.param),
meta: {},
}
})
}
// --- Роутер с middleware ---
class NuxtRouter {
constructor(routes) {
this.routes = routes
this.middlewares = {}
this.currentRoute = null
this.history = []
}
addMiddleware(name, fn) {
this.middlewares[name] = fn
}
matchRoute(url) {
// Убираем query string
const [pathname, queryString] = url.split('?')
const query = {}
if (queryString) {
queryString.split('&').forEach(part => {
const [k, v] = part.split('=')
if (k) query[decodeURIComponent(k)] = decodeURIComponent(v || '')
})
}
const urlSegs = pathname.split('/').filter(Boolean)
for (const route of this.routes) {
const routeSegs = route.path.split('/').filter(Boolean)
const params = {}
let matched = true
// Простое сопоставление (без catch-all для краткости)
if (routeSegs.length !== urlSegs.length && !route.path.includes('*')) {
matched = false
} else {
for (let i = 0; i < routeSegs.length; i++) {
if (routeSegs[i].startsWith(':')) {
params[routeSegs[i].slice(1).replace('?', '')] = urlSegs[i] || null
} else if (routeSegs[i] !== urlSegs[i]) {
matched = false
break
}
}
}
if (matched) return { route, params, query, fullPath: url }
}
return null
}
async navigate(to) {
const from = this.currentRoute
const match = typeof to === 'string'
? this.matchRoute(to)
: this.matchRoute(to.path + (to.query ? '?' + new URLSearchParams(to.query).toString() : ''))
if (!match) {
console.warn(`[Router] Маршрут не найден: ${JSON.stringify(to)}`)
return false
}
// Выполняем middleware
const middlewareList = match.route.meta.middleware || []
for (const name of middlewareList) {
const mw = this.middlewares[name]
if (!mw) continue
const result = await mw(match, from)
if (result && result.redirect) {
console.log(`[Middleware:${name}] Редирект → ${result.redirect}`)
return this.navigate(result.redirect)
}
}
this.history.push(this.currentRoute?.fullPath)
this.currentRoute = match
console.log(`[Router] Навигация → ${match.fullPath}`, match.params)
return true
}
back() {
const prev = this.history.pop()
if (prev) return this.navigate(prev)
}
}
// === Демо ===
const files = [
'index.vue',
'about.vue',
'users/index.vue',
'users/[id].vue',
'users/[id]/edit.vue',
'blog/index.vue',
'blog/[slug].vue',
'admin/index.vue',
'admin/settings.vue',
]
const routes = buildRoutesFromFiles(files)
console.log('=== Сгенерированные маршруты ===')
routes.forEach(r => console.log(` ${r.path.padEnd(25)} ← ${r.file}`))
const router = new NuxtRouter(routes)
// Добавляем middleware авторизации
router.addMiddleware('auth', async (to, from) => {
const isAuthenticated = to.route.path !== '/admin/index'
if (!isAuthenticated) return { redirect: '/login' }
})
// Навигация маршрута с meta
routes.find(r => r.path === '/admin').meta.middleware = ['auth']
console.log('\n=== Навигация ===')
router.navigate('/about')
router.navigate('/users/42')
router.navigate('/users/42/edit')
router.navigate('/blog/vue-3-guide?page=2')
console.log('\nТекущий маршрут:', router.currentRoute?.route.path)
console.log('Query:', router.currentRoute?.query)
router.back()
console.log('После back():', router.currentRoute?.route.path)
Nuxt генерирует маршруты автоматически из структуры папки pages/:
pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
├── blog/
│ ├── index.vue → /blog
│ └── [slug].vue → /blog/vue-3-tutorial
├── users/
│ ├── index.vue → /users
│ └── [id]/
│ ├── index.vue → /users/42
│ └── edit.vue → /users/42/edit
└── [...404].vue → /любой/несуществующий/путь<!-- pages/users/[id].vue -->
<script setup>
const route = useRoute()
const id = route.params.id // '42'
// Загрузка данных по параметру
const { data: user } = await useFetch(`/api/users/${id}`)
</script>
<template>
<div>
<h1>{{ user?.name }}</h1>
</div>
</template>pages/
└── users/
├── index.vue → /users (список)
└── [id].vue → /users/:id (детали)Вложенный layout через <NuxtPage>:
<!-- pages/users.vue — родительский layout -->
<template>
<div>
<Sidebar />
<NuxtPage /> <!-- дочерние маршруты рендерятся здесь -->
</div>
</template><!-- layouts/default.vue — применяется ко всем страницам -->
<template>
<div>
<Header />
<main>
<slot /> <!-- контент страницы -->
</main>
<Footer />
</div>
</template><!-- layouts/admin.vue — кастомный layout -->
<template>
<div class="admin">
<AdminSidebar />
<slot />
</div>
</template>// pages/admin/index.vue — использование кастомного layout
definePageMeta({ layout: 'admin' })// middleware/auth.ts — защита маршрутов
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value && to.path !== '/login') {
return navigateTo('/login') // редирект
}
})// Применение к конкретным страницам:
// pages/dashboard.vue
definePageMeta({
middleware: 'auth'
})
// Или к группе маршрутов:
definePageMeta({
middleware: ['auth', 'role-check']
})// Программная навигация
const router = useRouter()
const route = useRoute()
// Способ 1: navigateTo() (рекомендуется в Nuxt)
await navigateTo('/about')
await navigateTo({ name: 'users-id', params: { id: 42 } })
await navigateTo('https://example.com', { external: true })
// Способ 2: router.push()
router.push('/about')
router.replace('/about')
router.back()
// Чтение текущего маршрута
console.log(route.path) // '/users/42'
console.log(route.params.id) // '42'
console.log(route.query.page) // '2' (из ?page=2)Метаданные страницы (SSR, cache, layout, middleware):
definePageMeta({
layout: 'admin',
middleware: ['auth'],
ssr: false, // отключить SSR для этой страницы
title: 'Страница', // SEO заголовок
keepalive: true, // кэшировать компонент страницы
})Реализация файлового роутера — генерация маршрутов из дерева файлов и навигация с middleware
// Реализуем упрощённый файловый роутер Nuxt:
// генерируем маршруты из файловой структуры и обрабатываем навигацию.
// --- Генератор маршрутов из файлов ---
function buildRoutesFromFiles(files) {
return files.map(filePath => {
const withoutExt = filePath.replace(/\.vue$/, '')
const segments = withoutExt.split('/').map(seg => {
if (seg.startsWith('[...') && seg.endsWith(']'))
return { type: 'catchAll', param: seg.slice(4, -1) }
if (seg.startsWith('[[') && seg.endsWith(']]'))
return { type: 'optional', param: seg.slice(2, -2) }
if (seg.startsWith('[') && seg.endsWith(']'))
return { type: 'dynamic', param: seg.slice(1, -1) }
if (seg === 'index')
return { type: 'index' }
return { type: 'static', value: seg }
})
// Убираем финальный index
if (segments.at(-1)?.type === 'index') segments.pop()
const pathParts = segments.map(s => {
if (s.type === 'static') return s.value
if (s.type === 'dynamic') return ':' + s.param
if (s.type === 'optional') return ':' + s.param + '?'
if (s.type === 'catchAll') return ':' + s.param + '*'
return ''
}).filter(Boolean)
return {
path: '/' + pathParts.join('/'),
file: filePath,
params: segments.filter(s => s.type !== 'static' && s.type !== 'index').map(s => s.param),
meta: {},
}
})
}
// --- Роутер с middleware ---
class NuxtRouter {
constructor(routes) {
this.routes = routes
this.middlewares = {}
this.currentRoute = null
this.history = []
}
addMiddleware(name, fn) {
this.middlewares[name] = fn
}
matchRoute(url) {
// Убираем query string
const [pathname, queryString] = url.split('?')
const query = {}
if (queryString) {
queryString.split('&').forEach(part => {
const [k, v] = part.split('=')
if (k) query[decodeURIComponent(k)] = decodeURIComponent(v || '')
})
}
const urlSegs = pathname.split('/').filter(Boolean)
for (const route of this.routes) {
const routeSegs = route.path.split('/').filter(Boolean)
const params = {}
let matched = true
// Простое сопоставление (без catch-all для краткости)
if (routeSegs.length !== urlSegs.length && !route.path.includes('*')) {
matched = false
} else {
for (let i = 0; i < routeSegs.length; i++) {
if (routeSegs[i].startsWith(':')) {
params[routeSegs[i].slice(1).replace('?', '')] = urlSegs[i] || null
} else if (routeSegs[i] !== urlSegs[i]) {
matched = false
break
}
}
}
if (matched) return { route, params, query, fullPath: url }
}
return null
}
async navigate(to) {
const from = this.currentRoute
const match = typeof to === 'string'
? this.matchRoute(to)
: this.matchRoute(to.path + (to.query ? '?' + new URLSearchParams(to.query).toString() : ''))
if (!match) {
console.warn(`[Router] Маршрут не найден: ${JSON.stringify(to)}`)
return false
}
// Выполняем middleware
const middlewareList = match.route.meta.middleware || []
for (const name of middlewareList) {
const mw = this.middlewares[name]
if (!mw) continue
const result = await mw(match, from)
if (result && result.redirect) {
console.log(`[Middleware:${name}] Редирект → ${result.redirect}`)
return this.navigate(result.redirect)
}
}
this.history.push(this.currentRoute?.fullPath)
this.currentRoute = match
console.log(`[Router] Навигация → ${match.fullPath}`, match.params)
return true
}
back() {
const prev = this.history.pop()
if (prev) return this.navigate(prev)
}
}
// === Демо ===
const files = [
'index.vue',
'about.vue',
'users/index.vue',
'users/[id].vue',
'users/[id]/edit.vue',
'blog/index.vue',
'blog/[slug].vue',
'admin/index.vue',
'admin/settings.vue',
]
const routes = buildRoutesFromFiles(files)
console.log('=== Сгенерированные маршруты ===')
routes.forEach(r => console.log(` ${r.path.padEnd(25)} ← ${r.file}`))
const router = new NuxtRouter(routes)
// Добавляем middleware авторизации
router.addMiddleware('auth', async (to, from) => {
const isAuthenticated = to.route.path !== '/admin/index'
if (!isAuthenticated) return { redirect: '/login' }
})
// Навигация маршрута с meta
routes.find(r => r.path === '/admin').meta.middleware = ['auth']
console.log('\n=== Навигация ===')
router.navigate('/about')
router.navigate('/users/42')
router.navigate('/users/42/edit')
router.navigate('/blog/vue-3-guide?page=2')
console.log('\nТекущий маршрут:', router.currentRoute?.route.path)
console.log('Query:', router.currentRoute?.query)
router.back()
console.log('После back():', router.currentRoute?.route.path)
Реализуй функцию `createFileRouter(files)`, которая принимает массив путей файлов (относительно pages/) и возвращает объект с методами: `getRoutes()` — массив объектов `{ path, file }` (index.vue → убрать из пути, [param] → :param), `match(url)` — находит маршрут и извлекает параметры `{ route, params }` или null, `navigate(url, middlewares)` — применяет массив функций-middleware по цепочке (каждая получает to, from и может вернуть redirect-строку) и обновляет текущий маршрут.
В fileToPath: const segs = file.replace(".vue","").split("/"); const converted = segs.map(s => s.startsWith("[") && s.endsWith("]") ? ":" + s.slice(1,-1) : s); if (converted.at(-1) === "index") converted.pop(); return "/" + converted.join("/") || "/". В match: разбей url.split("/").filter(Boolean), сравни длины, для каждого сегмента маршрута: if (seg.startsWith(":")) params[seg.slice(1)] = urlSeg, иначе seg !== urlSeg → не совпало.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке