Nuxt 3 路由系统深度讲解
Nuxt 的自动路由是其最强大的特性之一。一个文件对应一个页面,无需手动配置,大大提升开发效率。
1. 文件系统路由基础
自动路由约定
pages/
├── index.vue → /
├── about.vue → /about
├── products.vue → /products
│
├── blog/
│ ├── index.vue → /blog
│ └── [id].vue → /blog/:id
│
├── admin/
│ ├── dashboard.vue → /admin/dashboard
│ └── users.vue → /admin/users
│
└── [...slug].vue → /* (catch-all)
路由优先级 (Route Priority)
优先级顺序 (从高到低):
1. 静态路由 /about.vue → /about (最精确)
2. 动态路由 /posts/[id].vue → /posts/123
3. 嵌套路由 /posts/[id].vue → /posts/123
4. Catch-all /[...slug].vue → /任何未匹配
示例匹配过程:
访问 /products/electronics/phones
routes 中的匹配顺序:
├─ /products.vue ✗
├─ /products/[category].vue ✗ (缺少 /phones)
├─ /products/[category]/[item].vue ✓ 匹配!
└─ /[...slug].vue (backup)
2. 动态路由详解
单个动态参数
<!-- pages/posts/[id].vue -->
<script setup lang="ts">
// 自动获取路由参数
const route = useRoute()
const id = route.params.id // String
// 响应式获取参数
const { data: post } = await useFetch(`/api/posts/${id}`)
</script>
<template>
<div>
<h1>{{ post?.title }}</h1>
<p>ID: {{ id }}</p>
</div>
</template>
<!-- 访问 /posts/123 时:
- id = "123"
- post 包含该文章内容
-->
多个动态参数
<!-- pages/[username]/posts/[postId].vue -->
<script setup lang="ts">
const route = useRoute()
const username = route.params.username
const postId = route.params.postId
// 类型安全
const { data } = await useFetch(
`/api/users/${username}/posts/${postId}`
)
</script>
<!-- 访问 /john/posts/42 时:
- username = "john"
- postId = "42"
匹配路径: /[username]/posts/[postId].vue
-->
可选参数 (Optional Parameters)
pages/
├── posts/
│ ├── [[id]].vue → /posts, /posts/123
│ └── [category]/[[tag]].vue → /electronics, /electronics/phones
<!-- pages/posts/[[id]].vue -->
<script setup lang="ts">
const route = useRoute()
// 如果 id 为空,则为 undefined 而非 null
const id = route.params.id || 'latest'
const postId = id === 'latest' ? 'LATEST_POST' : id
const { data: post } = await useFetch(`/api/posts/${postId}`)
</script>
<!-- 可用路由:
/posts → id = undefined
/posts/latest → id = "latest"
/posts/123 → id = "123"
-->
Catch-all 路由
pages/
└── [...slug].vue → 捕获所有未匹配的路由
<!-- pages/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// slug 是一个数组
const slug = route.params.slug // string[]
const path = slug.join('/')
// 例子:
// /docs/guides/installation → slug = ['docs', 'guides', 'installation']
// /404/missing/page → slug = ['404', 'missing', 'page']
</script>
<template>
<div v-if="slug && slug.length > 0">
<h1>页面: {{ path }}</h1>
</div>
<div v-else>
<h1>404 - 页面未找到</h1>
</div>
</template>
3. 嵌套路由和布局
文件结构对应的嵌套路由
pages/
├── parent/
│ ├── parent.vue → /parent (父级路由)
│ ├── child1.vue → /parent/child1
│ └── child2.vue → /parent/child2
使用布局分组 (Grouped Routes)
pages/
├── layouts/
│ ├── admin.vue (管理后台布局)
│ └── public.vue (公开页面布局)
│
├── admin/
│ ├── index.vue → /admin
│ ├── users.vue → /admin/users
│ └── settings.vue → /admin/settings
│
└── public/
├── about.vue → /public/about
└── contact.vue → /public/contact
4. 路由导航
编程式导航
const router = useRouter()
const route = useRoute()
// 简单导航
router.push('/about')
// 对象语法
router.push({
path: '/posts/123',
query: { sort: 'date', limit: 10 }
// 结果: /posts/123?sort=date&limit=10
})
// 命名路由 (通过 defineRouteRules)
router.push({
name: 'post',
params: { id: 123 }
})
// 相对导航
router.push('../parent') // 相对于当前路由
// 返回
router.back()
router.go(-1)
// 替换当前历史记录
router.replace('/home')
// 检查是否在特定路由
const isHomePage = route.path === '/'
const isAboutPage = route.name === 'about'
声明式导航
<template>
<!-- 基础链接 -->
<NuxtLink to="/about">关于</NuxtLink>
<!-- 对象语法 -->
<NuxtLink :to="{ path: '/posts', query: { sort: 'date' } }">
文章
</NuxtLink>
<!-- 动态链接 -->
<NuxtLink :to="`/posts/${id}`">{{ title }}</NuxtLink>
<!-- 活跃状态样式 -->
<NuxtLink
to="/admin"
active-class="bg-blue-500"
exact-active-class="text-bold"
>
管理后台
</NuxtLink>
</template>
5. 路由参数验证
验证动态参数
// utils/validateParams.ts
export function validatePostId(id: string): boolean {
return /^\d+$/.test(id) // 必须是纯数字
}
export function validateUsername(username: string): boolean {
return /^[a-zA-Z0-9_-]{3,20}$/.test(username) // 3-20 个字母数字
}
<!-- pages/posts/[id].vue -->
<script setup lang="ts">
import { validatePostId } from '~/utils/validateParams'
const route = useRoute()
const id = route.params.id as string
// 验证
if (!validatePostId(id)) {
throw createError({
statusCode: 400,
statusMessage: '无效的文章 ID'
})
}
const { data: post } = await useFetch(`/api/posts/${id}`)
</script>
6. 路由钩子和守卫
全局前置守卫
// app.vue or nuxt.config.ts
export default defineNuxtConfig({
// 导航守卫
hooks: {
// 路由改变前
'router:beforeEach': (to, from) => {
console.log(`从 ${from.path} 导航到 ${to.path}`)
// 可以返回 false 来阻止导航
// if (!isAuthenticated && to.path === '/admin') {
// return false
// }
},
// 路由改变后
'router:afterEach': (to, from) => {
console.log('导航完成')
}
}
})
中间件守卫
// middleware/auth.ts
export default defineRouteMiddleware((to, from) => {
const authStore = useAuthStore()
// 如果未认证,重定向到登录
if (!authStore.isAuthenticated) {
return navigateTo('/login')
}
})
// middleware/admin.ts
export default defineRouteMiddleware((to, from) => {
const user = useCurrentUser()
// 如果不是管理员,重定向
if (user.value?.role !== 'admin') {
throw createError({
statusCode: 403,
statusMessage: '禁止访问'
})
}
})
在页面中使用中间件
<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin']
})
// 该页面会在加载前检查认证和权限
</script>
7. 查询字符串和 URL 状态
获取和管理查询参数
const route = useRoute()
const router = useRouter()
// 读取查询参数
const page = route.query.page || '1'
const sort = route.query.sort || 'date'
const filters = route.query.filters // 可能是 string 或 string[]
// 设置查询参数
router.push({
path: route.path,
query: {
page: '2',
sort: 'price',
filters: ['electronics', 'books']
}
})
// 更新单个查询参数
router.push({
query: {
...route.query,
page: '2'
}
})
// 清除查询参数
router.push({
path: route.path
})
查询参数与状态同步
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const filters = ref<string[]>()
// 从 URL 初始化
onMounted(() => {
filters.value = Array.isArray(route.query.filters)
? route.query.filters
: route.query.filters ? [route.query.filters] : []
})
// 监听过滤器变化,更新 URL
watch(filters, (newFilters) => {
router.push({
query: {
...route.query,
filters: newFilters
}
})
}, { deep: true })
</script>
<template>
<!-- 用户修改过滤器时,URL 会自动更新 -->
<div>
<label v-for="option in filterOptions" :key="option">
<input
type="checkbox"
:value="option"
:checked="filters?.includes(option)"
@change="e => {
if (e.target.checked) {
filters = [...(filters || []), option]
} else {
filters = filters?.filter(f => f !== option) || []
}
}"
/>
{{ option }}
</label>
</div>
</template>
8. 高级路由配置
自定义路由规则
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// 预渲染 (生成静态 HTML)
'/': { prerender: true },
'/about': { prerender: true },
'/blog/**': { prerender: true },
// 缓存策略
'/api/**': { cache: { maxAge: 60 * 10 } }, // 缓存 10 分钟
'/static/**': { cache: { maxAge: 60 * 60 * 24 } }, // 缓存 1 天
// 重定向
'/old-page': { redirect: '/new-page' },
// 不索引
'/admin/**': { noindex: true },
// 自定义标头
'/secure/**': { headers: { 'X-Custom-Header': 'value' } }
}
})
路由懒加载
// Nuxt 3 默认对所有页面进行代码分割
// 不需要手动配置,自动实现:
// pages/posts/[id].vue
// ↓
// 构建时自动生成 posts-[id].chunk.js
// 按需加载时:
// /posts/1 → 加载 posts-[id].chunk.js
// /posts/2 → 复用已加载的 chunk
9. 路由最佳实践
清单
// ✅ 最佳实践
// 1. 使用文件系统路由
// ✅ 好
pages/
└── products/[id].vue
// ❌ 避免
// 手动在 router.js 中配置路由
// 2. 有意义的参数名
// ✅ 好
pages/[username]/[postId].vue
// ❌ 避免
pages/[a]/[b].vue
// 3. 使用中间件处理认证
// ✅ 好
middleware/auth.ts
definePageMeta({ middleware: 'auth' })
// ❌ 避免
// 在每个页面中手动检查认证
// 4. 利用查询参数保持状态
// ✅ 好
/products?sort=price&page=2
// ❌ 避免
// 存储在组件本地状态,刷新时丢失
// 5. 合理的嵌套深度
// ✅ 好
/users/john/posts/42
// ❌ 避免
// /a/b/c/d/e/f/g (太深)
总结
Nuxt 3 路由系统的核心优势:
| 特性 | 优势 | 使用场景 |
|---|---|---|
| 文件系统路由 | 零配置 | 所有应用 |
| 自动代码分割 | 性能优化 | 大型应用 |
| 中间件系统 | 灵活守卫 | 认证/权限 |
| 查询参数同步 | 保持状态 | 过滤/搜索 |
| 路由规则 | 完整控制 | 高级场景 |


