动态路由、嵌套路由与路由分组

真实项目的 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&#123; id: '123' &#125;
[[id]][[id]].vue/videos/videos/123&#123; id?: '123' &#125;
[...slug][...slug].vue/a/b/c 任意深度&#123; slug: ['a','b','c'] &#125;
多段[user]/[repo]/alice/my-app&#123; user: 'alice', repo: 'my-app' &#125;

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.vuestudio/ 同名共存——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 文件都需要包含 &lt;NuxtPage /&gt; 来渲染下一级子路由。

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/uploadupload.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 数组
  • 嵌套路由:同名文件 + 同名目录,父组件用 &lt;NuxtPage /&gt; 渲染子路由
  • 路由分组 (group)/:圆括号目录不影响 URL,用于文件组织和布局分配
  • 优先级:精确匹配 > 动态参数 > 可选参数 > catch-all

下一章深入路由中间件体系——全局/局部中间件、权限拦截、导航守卫。