03 / 09

动态路由为什么这样设计?

从实际 URL 推导动态参数、嵌套路由、catch-all 与可选参数的设计,并掌握 validate 和预渲染。

动态路由为什么这样设计?

学习目标

理解 Nuxt 动态路由的设计思想,而不是死记语法。

学完本章,你应该能够:

  • 理解 [id] 的含义
  • 理解多层动态路由为什么返回对象
  • 理解 [...slug] 为什么返回数组
  • 理解 [[optional]] 可选参数与前面三种的区别
  • validate 控制参数合法性
  • 看到文件名,就知道 params 长什么样

一、这个知识解决什么问题?

上一章我们已经知道 pages/about.vue/about文件路由的原理)。那如果页面不是固定的呢?

商品详情 /product/1001、用户详情 /user/888……难道要创建无数个文件?显然不现实。

于是,动态路由出现了。这一章的关键不是记住"怎么写 [id]",而是理解 Nuxt 为什么用 []... 这些符号,以及拿到 params 之后容易在哪里翻车。


二、我们的讨论过程

第一层:普通动态参数

创建 pages/product/[id].vue,访问 /product/1001/product/9527 都成功。

[id]占位符(Placeholder),Nuxt 会把 1001 保存到 route.params.id

route.params  // → { id: "1001" }

第二层:多参数

pages/[category]/[id].vue,访问 /frontend/1001

这是讨论最久的问题——为什么 route.params 不是嵌套对象?

因为 Nuxt 把每一个 [] 都看成一个参数,最终组成扁平的参数集合:

{ category: "frontend", id: "1001" }

params 是参数集合(Parameter Map),不是树。

第三层:Catch All

pages/[...slug].vue 访问 /docs/nuxt/router/install

route.params  // → { slug: ["docs", "nuxt", "router", "install"] }

为什么是数组?因为不知道到底有几层,Nuxt 只能全部收集起来。

第四层:可选参数

pages/[[slug]].vue,多了一对中括号。它表示"这段路径可有可无":

访问 URL params
/ {}slugundefined
/guides { slug: "guides" }
/a/b/c { slug: ["a", "b", "c"] }

有没有 ... 决定了是"单个可选段"还是"剩余路径全要"。[[slug]] 最典型的用途是让同一套页面既支持首页、也支持多级路径,但它的规则也最容易记混。

一个必须接受的设定:参数全是字符串

无论 URL 里写的是 1001 还是 9527,Nuxt 拿到的 params.id 都是字符串 "1001"

URL 本身没有数字类型,路径参数永远是 String。

需要做数学运算或比较时,自己转换:

const id = Number(route.params.id)

三、最终结论

Nuxt 的动态路由有四种写法:

文件名 访问 URL params 结果
[id].vue /1001 { id: "1001" }
[category]/[id].vue /frontend/1001 { category: "frontend", id: "1001" }
[...slug].vue /a/b/c { slug: ["a", "b", "c"] }
[[slug]].vue //a/b { slug: undefined }{ slug: ["a", "b"] }

四、脑图

Nuxt 动态路由脑图:从文件模板和 URL 推导 route.params

推导顺序:

  1. 把文件路径和实际 URL 逐段对齐
  2. 普通文字是固定路径,不进入 params
  3. 每个 [name] 取出对应的一段,成为 params.name
  4. [...name] 收集剩余所有段,得到数组
  5. [[name]] 同样收集,但允许零段匹配

记住:URL 参数默认都是字符串,多个 [] 组成扁平参数对象,不会因目录嵌套产生嵌套对象。


五、代码实验

创建 pages/product/[id].vue

<script setup>
const route = useRoute()
const id = Number(route.params.id)
</script>

<template>
  <p>当前商品 ID:{{ route.params.id }}(类型:{{ typeof route.params.id }})</p>
  <p>转换后:{{ id }}(类型:{{ typeof id }})</p>
</template>

访问 /product/1001,页面会显示 "1001" 是字符串、1001 是数字。

再试试给参数加校验。在 [id].vue 里补充:

definePageMeta({
  validate: (route) => {
    return /^\d+$/.test(route.params.id)
  }
})

访问 /product/1001 正常,访问 /product/abc 会直接进入 404,而不是让页面自己判断。这个机制比在组件里写 if 更早、更干净。

另外,路由参数经常是数据请求的输入。下一章会讲到参数变化如何自动触发重新请求(为什么 useFetch 不是 fetch)。


六、版本说明

本系列以 Nuxt 3.x 的默认目录结构为例:动态路由写在 pages/ 下。Nuxt 4 把页面目录移到了 app/pages/,但 [][...][[...]] 的推导规则完全一致,本节内容同样适用。


七、容易踩坑

[category]/[id] 会形成嵌套对象

不会,永远都是扁平的 { category, id }

[...slug] 是字符串

不是,永远是数组;取第一段要写 route.params.slug[0]

[id] 是 Number

不是,URL 参数全部都是 String,需要自己转换:Number(route.params.id)

[[slug]][...slug] 等价

不是。[[slug]] 允许零段匹配(根路径也命中),[...slug] 至少需要一段。

❌ 参数不合法就在组件里报错

更好的做法是用 validate 在路由层直接返回 404,避免无意义的渲染。

想对比 Nuxt 和 Next.js 在路由设计上的差异,可以看 Next.js 路由指南


一句话记忆

一个 [] 对应一个参数;... 收集剩余路径返回数组;[[...]] 表示这段可有可无;所有参数都是字符串。