文件路由
Nuxt 基于文件系统自动生成路由,无需手动配置路由表。这是 Nuxt 最核心的"约定"——创建文件就是创建路由。
为什么用文件路由?
| 手动配置路由(Vue Router) | 文件路由(Nuxt) |
|---|---|
创建 .vue 文件 | 创建 .vue 文件 |
在 router.ts 中写路由配置 | 不需要,自动生成 |
| 配置嵌套路由、动态路由 | 按目录结构约定,自动处理 |
| 忘了改路由配置 → 页面 404 | 不会忘,文件在路由就在 |
核心理念
文件路径 = 路由路径。你只需要关心"页面文件放在哪里",Nuxt 帮你处理路由配置。
基本路由
app/pages/ 目录下的 .vue 文件自动映射为路由:
| 文件路径 | 路由 | 说明 |
|---|---|---|
pages/index.vue | / | index.vue 是特殊文件名,代表目录的根路径 |
pages/about.vue | /about | 文件名即路由路径 |
pages/contact.vue | /contact | 同上 |
pages/blog/index.vue | /blog | 子目录的首页 |
pages/blog/hello.vue | /blog/hello | 子目录的子页面 |
index.vue 的特殊含义
无论在哪个目录下,index.vue 都代表该目录的根路由。就像网站首页总是 index.html 一样。
快速判断
把文件路径中的 app/pages 去掉,把 index.vue 替换为 /,就是路由路径。
动态路由
什么是动态路由?
有些页面的 URL 有一部分是"变化的"——比如博客文章 /blog/123、/blog/456,数字部分是文章 ID,但页面结构一样。这种"某一段是变量"的路由就是动态路由。
语法:[参数名]
用方括号包裹参数名,这个文件就能匹配该位置的任意值:
文件路径 → 路由模式 → 能匹配的 URL
─────────────────────────────────────────────────────────────────────────
pages/blog/[id].vue → /blog/:id → /blog/123
→ /blog/my-post
→ /blog/任何值
pages/user/[name].vue → /user/:name → /user/alice
→ /user/bob
pages/category/[slug].vue → /category/:slug → /category/tech
→ /category/life文件命名 → 路由模式的转换规则
| 文件命名 | 含义 | 对应的路由模式 | 说明 |
|---|---|---|---|
[id].vue | 这一段是变量,变量名是 id | /:id | 匹配一个路径段 |
[name].vue | 这一段是变量,变量名是 name | /:name | 方括号里的名字就是参数名 |
index.vue | 这是目录的默认页 | / | 无参数 |
about.vue | 固定路径 | /about | 无参数 |
简单记忆
[xxx] = "这里可以是任何值",方括号里的 xxx 是你给这个变量起的名字。
路由模式中的 :id 是什么?
这是 Vue Router 的动态路径参数语法(也叫"冒号参数")。/blog/:id 中的 :id 表示"这个位置是一个名为 id 的变量"。Nuxt 的工作就是把你写的 [id].vue 自动转换成 Vue Router 的 /:id 语法——你只需要会写 [id].vue,不需要手写 :id。
在页面中获取参数
<!-- app/pages/blog/[id].vue -->
<script setup>
const route = useRoute()
const id = route.params.id // 类型:string | string[]
</script>
<template>
<div>
<h1>博客文章</h1>
<p>文章 ID: {{ id }}</p>
</div>
</template>运行示例:
| 用户访问的 URL | route.params 的值 | 页面显示 |
|---|---|---|
/blog/123 | { id: '123' } | 文章 ID: 123 |
/blog/hello-world | { id: 'hello-world' } | 文章 ID: hello-world |
/blog/ | ❌ 不匹配 | 动态路由至少要有一个值 |
INFO
️ 常见初学者错误:在 [id].vue 模板中直接使用 id 但 <script> 中没有通过 useRoute() 定义 必须先 const route = useRoute() 再 const id = route.params.id,否则模板中 {{ id }} 会是 undefined
为什么 route.params.id 是 string 而不是 number?
URL 中的所有内容都是字符串。即使你访问 /blog/123,id 也是 '123'(string),不是 123(number)。如果需要数字,手动转换:
const id = Number(route.params.id)INFO
️ 注意:route.params 在 SSR 和客户端都能使用 是 SSR 安全的
一个文件中能有多个动态参数吗?
可以!每个路径段都可以独立是动态的:
文件路径 → 路由模式 → 访问示例
──────────────────────────────────────────────────────────────────────────
pages/blog/[category]/[id].vue → /blog/:category/:id → /blog/tech/123
pages/shop/[shopId]/product/[pid].vue → /shop/:shopId/product/:pid → /shop/5/product/42获取多个参数:
<!-- app/pages/blog/[category]/[id].vue -->
<script setup>
const route = useRoute()
const category = route.params.category // 'tech'
const id = route.params.id // '123'
</script>
<template>
<div>
<p>分类:{{ category }}</p>
<p>文章 ID:{{ id }}</p>
</div>
</template>典型场景
- 博客文章:
/blog/[id].vue→/blog/my-first-post - 用户资料:
/user/[name].vue→/user/alice - 商品详情:
/product/[slug].vue→/product/nuxt-t-shirt
通配路由
什么是通配路由?
动态路由 [id] 只能匹配一个路径段(如 /blog/123),但有时候你不知道 URL 有多少层——比如文档系统 /docs/a、/docs/a/b、/docs/a/b/c 都要匹配。这就需要通配路由。
语法:[...slug]
在参数名前加 ...,表示"匹配剩余的所有路径段":
文件路径 → 路由模式 → 能匹配的 URL
─────────────────────────────────────────────────────────────────────────────────
pages/docs/[...path].vue → /docs/:path(.*)* → /docs/a ✅
→ /docs/a/b ✅
→ /docs/a/b/c/d ✅
→ /docs/任意/多/层 ✅
pages/user/[...slug].vue → /user/:slug(.*)* → /user/alice ✅
→ /user/alice/settings ✅
→ /user/alice/settings/pwd ✅[id] vs [...slug] 对比
动态路由 [id] | 通配路由 [...slug] | |
|---|---|---|
| 语法 | [参数名] | [...参数名] |
| 匹配 | 一个路径段 | 任意数量路径段(≥1) |
| 参数类型 | string | string[](数组) |
| 路由模式 | /:id | /:slug(.*)* |
| 例子 | /blog/123 ✅ /blog/1/2 ❌ | /docs/a ✅ /docs/a/b/c ✅ |
| 能否匹配空值 | ❌ /blog/ 不匹配 | ❌ /docs/ 不匹配(至少要一段) |
INFO
路由模式中的 (.*)* 是什么意思? 这是 Vue Router 内部使用的正则表达式 (.*)* 表示"匹配任意字符零次或多次"。你不需要手写这个——Nuxt 看到 [...slug] 就会自动生成 /:slug(.*)*。你只需要理解
| 你写的文件名 | Nuxt 自动生成的路由模式 | 含义 |
|---|---|---|
[id].vue | /:id | 匹配 1 个路径段 |
[...slug].vue | /:slug(.*)* | 匹配任意多个路径段 |
[[...slug]].vue | /:slug(.*)* | 匹配 0 或多个路径段 |
在页面中获取通配参数
通配路由的参数是数组,不是字符串:
<!-- app/pages/docs/[...path].vue -->
<script setup>
const route = useRoute()
const pathSegments = route.params.path // 类型:string[]
// 拼接回完整路径
const fullPath = (pathSegments as string[]).join('/')
</script>
<template>
<div>
<h1>文档页面</h1>
<p>路径段:{{ pathSegments }}</p>
<p>完整路径:{{ fullPath }}</p>
</div>
</template>运行示例:
| 用户访问的 URL | route.params.path | 页面显示 |
|---|---|---|
/docs/getting-started | ['getting-started'] | 路径段:getting-started |
/docs/api/composables | ['api', 'composables'] | 路径段:api, composables |
/docs/a/b/c/d | ['a', 'b', 'c', 'd'] | 路径段:a, b, c, d |
INFO
️ 通配参数是数组:动态路由 [id] 的参数是 string 通配路由 [...slug] 的参数是 string[]。用的时候注意类型差异
// 动态路由 [id]
route.params.id // string | string[]
// 通配路由 [...slug]
route.params.slug // string[] (总是数组)
// 安全取值方式
const segments = Array.isArray(route.params.slug)
? route.params.slug
: [route.params.slug]可选通配路由:[[...slug]]
双括号 [[...slug]] 表示通配路由是可选的——即使没有额外路径段也能匹配:
文件路径 → 路由模式 → 能匹配的 URL
─────────────────────────────────────────────────────────────────────────────────
pages/docs/[[...path]].vue → /docs/:path(.*)* → /docs ✅ 匹配!
→ /docs/a ✅
→ /docs/a/b ✅[...slug] vs [[...slug]]
必选通配 [...slug] | 可选通配 [[...slug]] | |
|---|---|---|
/docs | ❌ 不匹配 | ✅ 匹配(slug 为空数组 []) |
/docs/a | ✅ 匹配 | ✅ 匹配 |
| 参数值 | 至少有一个元素 | 可能是空数组 [] |
通配路由适合"不确定有多少层级"的场景
比如文档系统可能有 /docs/getting-started,也可能有 /docs/api/composables/use-fetch。
三种路由参数语法完整对比
| 语法 | 文件示例 | 匹配规则 | 参数类型 | 匹配示例 | 不匹配示例 |
|---|---|---|---|---|---|
[param] | [id].vue | 匹配 1 个路径段 | string | /blog/123 → { id: '123' } | /blog/、/blog/a/b |
[...param] | [...slug].vue | 匹配 ≥1 个路径段 | string[] | /docs/a/b → { slug: ['a', 'b'] } | /docs/ |
[[...param]] | [[...slug]].vue | 匹配 ≥0 个路径段 | string[] | /docs → { slug: [] } | — |
嵌套路由
使用同名文件 + 同名目录创建嵌套路由:
pages/
├── users/
│ ├── index.vue → /users(用户列表)
│ └── [id].vue → /users/:id(用户详情)
└── users.vue → users 的父组件(必须包含 <NuxtPage />)嵌套路由的关键
users.vue 文件和 users/ 目录同时存在,Nuxt 就会创建嵌套路由。父组件(users.vue)必须包含 <NuxtPage />,子页面的内容在这里渲染。
pages/users.vue(父组件):
<template>
<div>
<h1>用户管理</h1>
<!-- 子页面(users/index.vue 或 users/[id].vue)在这里渲染 -->
<NuxtPage />
</div>
</template>pages/users/index.vue:
<template>
<div>用户列表</div>
</template>pages/users/[id].vue:
<script setup>
const route = useRoute()
</script>
<template>
<div>用户详情:{{ route.params.id }}</div>
</template>嵌套路由的渲染过程
- 用户访问
/users/123 - Nuxt 渲染
users.vue(父组件) - 在
<NuxtPage />的位置渲染[id].vue(子组件)
父组件始终显示,子组件在父组件的 <NuxtPage /> 位置切换。
什么时候用嵌套路由?
- 多个页面共享同一个"外壳"(如用户管理的侧边导航)
- 不想用布局(布局是全局的,嵌套路由是局部的)
- 子页面之间需要共享状态或导航
嵌套路由的目录结构
pages/
├── parent/ → 子路由内容
│ ├── child.vue → /parent/child
│ └── another.vue → /parent/another
└── parent.vue → 父路由(必须包含 <NuxtPage />)INFO
️ 常见错误:创建了 pages/users/ 目录但忘了创建 pages/users.vue 此时子路由不会嵌套,而是直接渲染
页面命名规则
路由名称根据文件路径自动生成:
| 文件路径 | 路由名称 | 规则 |
|---|---|---|
pages/index.vue | index | 根页面名就是 index |
pages/about.vue | about | 文件名即路由名 |
pages/blog/[id].vue | blog-id | 动态参数用 - 代替 / |
pages/user/[name].vue | user-name | 同上 |
路由名称有什么用?
在 navigateTo 和 router.push 中可以用名称导航,而不是硬编码路径:
router.push({ name: 'blog-id', params: { id: '123' } })好处是:如果路径变了(如 /blog 改为 /articles),只要路由名称不变,代码不用改。
禁用文件路由
如果你不需要文件路由(如单页应用),可以删除 pages/ 目录,只用 app.vue。
什么时候不需要文件路由?
- 纯 SPA 应用(不需要 SSR)
- 只有一个页面的应用(如登录页)
- 路由完全由代码控制
删除 pages/ 目录后,<NuxtPage /> 不再需要,app.vue 直接写内容。
自定义路由
如果文件路由不满足需求,可以在 nuxt.config.ts 中使用 hooks 自定义:
export default defineNuxtConfig({
hooks: {
'pages:extend'(pages) {
// 添加自定义路由
pages.push({
name: 'custom',
path: '/custom-route',
file: '~/app/pages/custom.vue',
})
// 移除某个路由
const index = pages.findIndex(p => p.path === '/remove-me')
if (index > -1) pages.splice(index, 1)
},
},
})pages:extend 钩子
在 Nuxt 生成路由后、应用启动前执行,你可以修改路由数组。这在以下场景有用:
- 根据环境变量决定是否注册某个路由
- 动态添加/移除路由
- 修改路由的元信息
路由验证
使用 definePageMeta.validate 验证动态参数:
<script setup>
definePageMeta({
validate: async (route) => {
// 必须是数字
return /^\d+$/.test(route.params.id as string)
},
})
</script>验证失败会跳转到 404 页面。
为什么需要路由验证?
- 防止用户输入非法 URL(如
/blog/abc而不是/blog/123) - 提前拦截无效请求,不用在页面逻辑中处理
- 比在页面中判断更早(在路由层就拦截了)
常见问题
创建了页面但访问 404
- 文件是否在
app/pages/下(不是根目录的pages/) app.vue中是否有<NuxtPage />- 重启开发服务器
嵌套路由子页面不显示
- 是否同时存在
users.vue和users/目录 - 父组件中是否有
<NuxtPage /> - 子页面的路径是否正确
动态路由参数类型不对
route.params 中的值总是 string 或 string[],需要手动转换类型。
知识脉络
核心概念 → 你在这里:文件路由
│
├─→ 下一步:路由导航
│
├─→ 下一步:路由中间件
│
└─→ 相关:视图与布局(05章)