浏览器持棒后跑的是哪条道?答案是构建期就画好的路由表。本节讲 pages 目录的命名语法如何推导路由——动态段、可选段、嵌套、catch-all 全集,末尾给校验路由表的方法。它是 5.2 守卫与 5.3 导航的地基,也是 Nuxt"约定优于配置"最密集的舞台。
文件路径即路由地址,特殊命名有特殊含义:
| 目录/文件 | 生成的路由 | 说明 |
|---|---|---|
| pages/index.vue | / | 首页 |
| pages/about.vue | /about | 静态路由 |
| pages/products/index.vue | /products | 目录式首页 |
| pages/products/[id].vue | /products/:id | 动态段,id 是参数 |
| pages/products-[group].vue | /products-:group() | 前缀拼接动态段(较新语法) |
| pages/products/[id]/reviews.vue | /products/:id/reviews | 动态段的下级 |
| pages/[[slug]].vue | / 与 /:slug 双匹配 | 可选动态段 |
| pages/users-[id]/profile.vue | /users-:id/profile | 组合示例 |
| pages/[...slug].vue | /:slug(.) | catch-all,兜底 404 之前的自定义页 |
动手搭一套商品站路由:
pages/ ├── index.vue # / ├── about.vue # /about ├── products/ │ ├── index.vue # /products │ └── [id].vue # /products/123 └── docs/[...slug].vue # /docs/a/b/c 任意深度
动态段页面里取参数:
<!-- pages/products/[id].vue --> <script setup> const route = useRoute() const id = computed(() => route.params.id) const { data: product } = await useFetch(() => `/api/products/${id.value}`, { key: () => `product-${id.value}`, // 参数进 key watch: [id], // 同一组件实例下 id 变化自动重取 }) </script> <template> <div v-if="product"> <h1>{{ product.name }}</h1> <p>编号:{{ id }}</p> </div> </template>
watch: [id] 值得注意:从 /products/1 点进 /products/2 时,两个地址匹配同一个页面组件,组件不销毁重建,只有参数变化——不监听 id 的话,数据停在旧商品上,这是内容站最常见的"换页不换数据"事故。

嵌套路由的规则稍绕:同名目录 + 同名文件构成父子关系。products.vue 与 products/ 目录同时存在时,products.vue 是父组件(负责外框),目录内的页面是子组件(填进 NuxtPage):
<!-- pages/products.vue:父路由,提供商品域的外框 --> <template> <div class="products-shell"> <CategoryNav /> <NuxtPage /> <!-- 子路由页面渲染在这里 --> </div> </template>
pages/ ├── products.vue # 父:/products 外框 └── products/ ├── index.vue # 子:/products 首页填进父的 NuxtPage └── [id].vue # 子:/products/123 详情填进父的 NuxtPage
没有 products.vue 时,products/ 下的页面各自全页渲染——目录只是分组,不产生嵌套。要不要父组件,取决于"这组页面是否共享一个外框"。
路由表不必靠脑内推导验证。开发模式下访问任意路径,DevTools 的路由面板直接展示记录;也可以在插件里打印:
// plugins/debug-routes.ts(开发环境专用) export default defineNuxtPlugin(() => { if (import.meta.dev) { const router = useRouter() console.table(router.getRoutes().map(r => ({ path: r.path, name: r.name, }))) } })
排错两条经验:其一,页面不生效先查文件名拼写([id] 写成 (id)、... 少一个点都是静默失败);其二,路由优先级是"具体路径优先于动态段、动态段优先于 catch-all",同时存在 about.vue 与 [slug].vue 时 /about 走静态页。
⚠️ 常见坑:动态参数页面忘记 watch。/products/1 到 /products/2 组件复用不重建,useFetch 不监听参数就永远显示第一个商品。参数一律进 key 并加 watch。
💡 关键直觉:把 pages 目录当数据库的"路由 schema"读——文件名是主键语法,方括号是通配符,父子目录是外键。schema 定稿于构建期,运行期只查表不建表。
页面过百后,扁平的 pages 目录会失控。三个组织技巧:其一,用目录分组业务域(products/、orders/、account/ 各自成林),路由深度与信息架构对齐,新人找页面像查目录;其二,catch-all 只留一个兜底(比如 docs 目录下的 [...slug]),全站多个 catch-all 互相遮蔽是难查的路由 bug;其三,废弃页面用重定向而不是删文件——路由的旧地址可能已被外部引用与收录,在服务端配置旧路径到新路径的 301(Nitro 路由规则一行搞定),SEO 与用户体验双保全。
[id] 动态段、[[slug]] 可选段、[...slug] catch-all、users-[group] 拼接段;