动态路由、嵌套路由与路由分组
真实项目的 URL 不会都是静态的——视频详情页需要
/videos/:id、用户主页需要/users/:username、CMS 系统需要/:slug+任意深度路径。本章深入 Nuxt4 的动态路由参数、嵌套路由布局、路由分组等高级路由模式,结合 AI 视频项目给出完整实战方案。
1. 动态路由参数 [id].vue
1.1 基本用法
用方括号 [] 包裹文件名中的部分,即成为动态参数:
app/pages/
├── videos/
│ └── [id].vue → /videos/:id
├── users/
│ └── [username].vue → /users/:username
└── categories/
└── [slug].vue → /categories/:slug
页面中通过 useRoute() 获取参数:
<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const videoId = route.params.id // string
const { data: video } = await useFetch(`/api/videos/${videoId}`)
</script>
<template>
<div v-if="video">
<h1>{{ video.title }}</h1>
<VideoPlayer :url="video.url" />
</div>
</template>1.2 多段动态参数
一个路径中可以有多个动态段:
app/pages/
└── users/
└── [userId]/
└── videos/
└── [videoId].vue → /users/:userId/videos/:videoId
<script setup lang="ts">
const route = useRoute()
const { userId, videoId } = route.params
// userId: string, videoId: string
</script>1.3 同一目录下的静态路由优先级
当静态路由和动态路由共存时,静态路由优先匹配:
app/pages/videos/
├── upload.vue → /videos/upload (静态,优先匹配)
├── trending.vue → /videos/trending (静态,优先匹配)
└── [id].vue → /videos/:id (动态,兜底匹配)
访问 /videos/upload → 匹配 upload.vue
访问 /videos/trending → 匹配 trending.vue
访问 /videos/abc123 → 匹配 [id].vue
1.4 Typed Pages 类型安全
启用 experimental.typedPages 后,动态参数获得类型推导:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: { typedPages: true }
})<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
const route = useRoute('videos-id') // 传入路由名称
route.params.id // ✅ string
route.params.slug // ❌ TS 错误:不存在 slug
// navigateTo 也有类型检查
navigateTo({
name: 'videos-id',
params: { id: '123' }, // ✅ 类型正确
})
</script>2. 可选参数与 catch-all 路由
2.1 可选参数 [[optional]].vue
双重方括号 [[]] 表示参数可选——有参数和没参数都能匹配:
app/pages/
└── videos/
└── [[page]].vue → /videos 和 /videos/2 都匹配
<!-- app/pages/videos/[[page]].vue -->
<script setup lang="ts">
const route = useRoute()
const page = Number(route.params.page) || 1
// /videos → page = 1(参数不存在,走默认值)
// /videos/3 → page = 3
</script>典型场景:分页列表——/videos 显示第 1 页,/videos/2 显示第 2 页。
2.2 catch-all 路由 [...slug].vue
三个点 ... 表示捕获所有剩余路径段:
app/pages/
├── docs/
│ └── [...slug].vue → /docs 以及 /docs/a/b/c 任意深度
└── [...404].vue → 自定义 404(匹配所有未知路径)
<!-- app/pages/docs/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// /docs/getting-started/install
// route.params.slug = ['getting-started', 'install'](数组!)
const slug = (route.params.slug as string[]) || []
const path = slug.join('/')
</script>注意:catch-all 参数是数组,不是字符串。
2.3 catch-all 的常见用途
用途 1:CMS 内容路由
<!-- app/pages/blog/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
const slug = (route.params.slug as string[])?.join('/') || 'index'
const { data: article } = await useFetch(`/api/cms/articles/${slug}`)
if (!article.value) {
throw createError({ statusCode: 404, message: '文章不存在' })
}
</script>用途 2:自定义 404 页面
<!-- app/pages/[...404].vue -->
<script setup lang="ts">
definePageMeta({ layout: false })
throw createError({
statusCode: 404,
statusMessage: '页面不存在',
fatal: true,
})
</script>2.4 参数语法速查表
| 语法 | 示例文件 | 匹配路径 | params 类型 |
|---|---|---|---|
[id] | [id].vue | /videos/123 | { id: '123' } |
[[id]] | [[id]].vue | /videos 或 /videos/123 | { id?: '123' } |
[...slug] | [...slug].vue | /a/b/c 任意深度 | { slug: ['a','b','c'] } |
| 多段 | [user]/[repo] | /alice/my-app | { user: 'alice', repo: 'my-app' } |
3. 嵌套路由 parent/child 模式
3.1 什么是嵌套路由
当 URL 有层级关系,且父级页面需要保留一个公共布局区域(如侧边栏、Tab 栏),子页面只替换部分内容时,就需要嵌套路由。
在 Nuxt4 中,嵌套路由通过同名文件 + 同名目录实现:
app/pages/
├── studio.vue ← 父路由组件(包含 <NuxtPage />)
└── studio/ ← 子路由目录
├── index.vue → /studio (默认子路由)
├── projects.vue → /studio/projects
└── settings.vue → /studio/settings
关键:studio.vue 和 studio/ 同名共存——studio.vue 是父级布局,studio/ 下的文件是子路由。
3.2 父路由组件
<!-- app/pages/studio.vue — 父路由 -->
<template>
<div class="flex h-screen">
<!-- 侧边栏(所有子页面共享) -->
<aside class="w-64 border-r p-4">
<h2 class="text-lg font-bold mb-4">创作工作台</h2>
<nav class="space-y-2">
<NuxtLink to="/studio" class="block px-3 py-2 rounded hover:bg-gray-100">
概览
</NuxtLink>
<NuxtLink to="/studio/projects" class="block px-3 py-2 rounded hover:bg-gray-100">
我的项目
</NuxtLink>
<NuxtLink to="/studio/settings" class="block px-3 py-2 rounded hover:bg-gray-100">
设置
</NuxtLink>
</nav>
</aside>
<!-- 子路由内容区 -->
<main class="flex-1 p-6">
<NuxtPage /> <!-- 👈 子路由渲染在这里 -->
</main>
</div>
</template>3.3 子路由组件
<!-- app/pages/studio/index.vue — /studio 默认页 -->
<template>
<div>
<h1>工作台概览</h1>
<div class="grid grid-cols-3 gap-4 mt-4">
<StatsCard title="我的视频" :count="12" />
<StatsCard title="总播放量" :count="34500" />
<StatsCard title="今日收入" :count="128" unit="元" />
</div>
</div>
</template><!-- app/pages/studio/projects.vue — /studio/projects -->
<template>
<div>
<h1>我的项目</h1>
<ProjectList />
</div>
</template>3.4 多级嵌套
嵌套可以是多层的:
app/pages/
├── admin.vue ← 第 1 级
└── admin/
├── index.vue → /admin
├── videos.vue ← 第 2 级
└── videos/
├── index.vue → /admin/videos
└── [id].vue → /admin/videos/:id
每一级的 .vue 文件都需要包含 <NuxtPage /> 来渲染下一级子路由。
3.5 AI 视频项目的嵌套路由设计
app/pages/
├── (main)/ ← 分组:主站布局
│ ├── index.vue → /
│ └── videos/
│ ├── index.vue → /videos
│ └── [id].vue → /videos/:id
├── studio.vue ← 工作台父路由
└── studio/
├── index.vue → /studio
├── create.vue → /studio/create
├── projects.vue → /studio/projects
└── analytics.vue → /studio/analytics
├── admin.vue ← 管理后台父路由
└── admin/
├── index.vue → /admin
├── users.vue → /admin/users
├── videos.vue → /admin/videos
└── settings.vue → /admin/settings
4. 路由分组 (group)/ 不影响 URL
4.1 为什么需要路由分组
有时候你需要组织文件结构,但不想影响 URL。比如登录和注册页面在逻辑上属于"认证"模块,但 URL 不应该是 /auth/login,而是 /login。
圆括号 () 包裹的目录名不会出现在 URL 中:
app/pages/
├── (auth)/ ← 分组:认证相关
│ ├── login.vue → /login (不是 /auth/login)
│ ├── register.vue → /register
│ └── forgot-password.vue → /forgot-password
├── (main)/ ← 分组:主站页面
│ ├── index.vue → /
│ ├── about.vue → /about
│ └── pricing.vue → /pricing
└── (dashboard)/ ← 分组:用户中心
├── profile.vue → /profile
└── settings.vue → /settings
4.2 分组 + 布局组合
分组的最大价值在于为不同组的页面指定不同布局:
app/pages/
├── (auth)/ → 使用 auth 布局(无导航栏)
│ ├── login.vue
│ └── register.vue
├── (main)/ → 使用 default 布局
│ ├── index.vue
│ └── about.vue
└── (studio)/ → 使用 studio 布局
├── studio.vue
└── studio/
├── index.vue
└── projects.vue
在分组内的页面中指定布局:
<!-- app/pages/(auth)/login.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'auth', // 认证页面使用无导航栏布局
})
</script>4.3 分组 + 中间件组合
也可以为整个分组应用中间件:
// app/middleware/auth-redirect.global.ts
export default defineNuxtRouteMiddleware((to) => {
const { isLoggedIn } = useAuth()
// 已登录用户访问认证页面,重定向到首页
const authPages = ['/login', '/register', '/forgot-password']
if (isLoggedIn.value && authPages.includes(to.path)) {
return navigateTo('/')
}
})4.4 分组的嵌套
分组可以嵌套使用:
app/pages/
└── (marketing)/
├── (landing)/
│ ├── index.vue → /
│ └── features.vue → /features
└── (legal)/
├── terms.vue → /terms
└── privacy.vue → /privacy
多层分组都不会影响 URL,它们只是文件组织方式。
5. 路由优先级与冲突解决
5.1 匹配优先级
当多个路由可能匹配同一个 URL 时,Nuxt4 按以下优先级排序:
优先级从高到低:
1. 精确匹配(静态路径) → /videos/upload
2. 动态参数(单段) → /videos/:id
3. 可选参数 → /videos/:id?
4. catch-all(多段) → /videos/:slug+
5.2 常见冲突场景
app/pages/videos/
├── upload.vue → /videos/upload ✅ 精确匹配,优先
├── [id].vue → /videos/:id ✅ 动态参数,次之
└── [...slug].vue → /videos/* ✅ catch-all,兜底
访问 /videos/upload → upload.vue(精确匹配)
访问 /videos/abc123 → [id].vue(动态参数)
访问 /videos/a/b/c → [...slug].vue(catch-all)
5.3 调试路由匹配
使用 DevTools 的 Pages 面板可以查看完整的路由表和当前匹配结果。也可以在终端中检查:
# 查看生成的路由表
cat .nuxt/routes.mjs本章小结
- 动态参数
[id]:单段匹配,route.params.id为 string - 可选参数
[[id]]:有无参数都匹配,适合分页场景 - catch-all
[...slug]:任意深度匹配,params 为 string 数组 - 嵌套路由:同名文件 + 同名目录,父组件用
<NuxtPage />渲染子路由 - 路由分组
(group)/:圆括号目录不影响 URL,用于文件组织和布局分配 - 优先级:精确匹配 > 动态参数 > 可选参数 > catch-all
下一章深入路由中间件体系——全局/局部中间件、权限拦截、导航守卫。