路由中间件
路由中间件在导航到特定路由前运行,用于权限检查、数据预加载等。
为什么需要路由中间件?
| 没有中间件 | 有中间件 |
|---|---|
| 每个页面单独写登录检查 | 中间件写一次,多个页面复用 |
| 容易遗漏某个页面 | 只需在 definePageMeta 中注册 |
| 权限逻辑散落各处 | 集中管理,统一维护 |
路由中间件的本质
一个在路由跳转前自动执行的函数。你可以决定:放行、重定向、还是阻止导航。
中间件类型
| 类型 | 命名规则 | 运行范围 | 典型用途 |
|---|---|---|---|
| 全局中间件 | .global.ts 结尾 | 每次路由变化都执行 | 日志、全局认证 |
| 页面中间件 | 普通文件名 | 仅注册了该中间件的页面 | 特定页面的权限检查 |
| 布局中间件 | 在布局中注册 | 使用该布局的所有页面 | 管理后台的全局认证 |
创建中间件
全局中间件
文件名以 .global. 结尾:
// app/middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 每次路由变化都执行
console.log(`导航:${from.path} → ${to.path}`)
})INFO
️ 全局中间件要谨慎使用:它会在每次路由变化时执行 包括首次加载和客户端导航。如果逻辑太重,会拖慢所有页面的加载速度
适合全局中间件的场景
- 路由日志/分析
- 全局认证(大多数页面都需要登录)
- A/B 测试分流
页面级中间件
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const isAuthenticated = useState('isAuthenticated')
// 未登录则跳转到登录页
if (!isAuthenticated.value && to.path !== '/login') {
return navigateTo('/login')
}
})为什么要排除 /login?
否则会导致死循环:未登录 → 跳转 /login → 中间件再次检查 → 还是未登录 → 再次跳转 → ...
更好的做法
检查目标路由是否需要认证,而不是检查当前状态:
// 只对需要认证的页面生效(通过 definePageMeta 的 meta 标记)
if (to.meta.requiresAuth && !isAuthenticated.value) {
return navigateTo('/login')
}注册中间件
在页面中注册
<script setup>
definePageMeta({
// 单个中间件
middleware: 'auth',
// 多个中间件(按顺序执行)
middleware: ['auth', 'admin'],
})
</script>中间件名称规则
名称基于文件名,去掉 .ts 后缀。如 auth.ts → 'auth',admin-check.ts → 'admin-check'。
内联中间件
<script setup>
definePageMeta({
middleware: [
'auth', // 引用 app/middleware/auth.ts
(to, from) => {
// 内联中间件逻辑(不用单独创建文件)
if (to.path.includes('/admin')) {
return navigateTo('/forbidden')
}
},
],
})
</script>什么时候用内联中间件?
逻辑非常简单且只在一个页面使用时。如果逻辑复杂或在多个页面使用,还是创建单独的文件更好。
在布局中注册
<!-- app/layouts/admin.vue -->
<script setup>
definePageMeta({
middleware: 'admin',
})
</script>
<template>
<div>
<AdminSidebar />
<NuxtPage />
</div>
</template>布局中间件的效果
所有使用 admin 布局的页面都会自动经过 admin 中间件。不需要在每个页面单独注册。
中间件执行顺序
1. 全局中间件(按文件名排序)
↓
2. 布局中间件(definePageMeta 中定义)
↓
3. 页面中间件(definePageMeta 中定义)
↓
4. 路由导航完成全局中间件的排序
如果有多个全局中间件,按文件名字母顺序执行。可以用数字前缀控制顺序:
middleware/
├── 01.log.global.ts # 第一个执行
├── 02.auth.global.ts # 第二个执行
└── analytics.global.ts # 按字母顺序中间件返回值
export default defineNuxtRouteMiddleware((to, from) => {
// 1. 无返回值 / 返回 undefined → 允许导航(继续)
return
// 2. 返回 navigateTo() → 重定向到其他页面
return navigateTo('/login')
// 3. 返回 abortNavigation() → 阻止导航(留在当前页面)
return abortNavigation()
// 4. 带错误信息阻止
return abortNavigation(
createError({ statusCode: 403, statusMessage: 'Forbidden' })
)
})navigateTo vs abortNavigation 的区别
navigateTo:"别去那了,去这里"(重定向)abortNavigation:"别去了,留在这"(阻止导航)
INFO
️ 必须有 return
// ❌ 错误:没有 return,导航不会被阻止
navigateTo('/login')
// ✅ 正确
return navigateTo('/login')常见中间件示例
认证中间件
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const token = useCookie('auth-token')
if (!token.value && to.path !== '/login') {
return navigateTo('/login', {
// 登录后返回原页面
query: { redirect: to.fullPath },
})
}
})query: { redirect: to.fullPath } 的作用
用户访问 /dashboard 被拦到 /login?redirect=/dashboard,登录成功后可以跳回 /dashboard。
登录页面的逻辑:
const route = useRoute()
async function onLoginSuccess() {
await navigateTo(route.query.redirect as string || '/')
}管理员中间件
// app/middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
const user = useState('currentUser')
if (user.value?.role !== 'admin') {
return abortNavigation(
createError({ statusCode: 403, statusMessage: 'Forbidden', message: '需要管理员权限' })
)
}
})日志中间件
// app/middleware/log.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`[Navigation] ${from.path} → ${to.path}`)
})会员中间件
// app/middleware/vip.ts
export default defineNuxtRouteMiddleware(() => {
const user = useState('currentUser')
if (!user.value?.isVip) {
return navigateTo('/subscribe')
}
})注意事项
1. 中间件文件自动注册
app/middleware/ 下的文件自动注册,无需手动 import。
2. SSR 兼容
中间件在 SSR 和客户端都会执行。所以:
- 不要在中间件中使用浏览器 API(
window、document) - 使用 SSR 安全的方法(
useCookie、useState、navigateTo)
3. 避免副作用
// ❌ 不好的做法:中间件修改状态
export default defineNuxtRouteMiddleware(() => {
const count = useState('count')
count.value++ // 有副作用!每次导航都会改变状态
})
// ✅ 好的做法:中间件只做检查和重定向
export default defineNuxtRouteMiddleware(() => {
const isAuthenticated = useCookie('auth-token')
if (!isAuthenticated.value) {
return navigateTo('/login')
}
})4. 命名冲突
中间件名称基于文件名,避免重复命名。如果两个文件叫 auth.ts(在不同子目录下),可能冲突。
5. 中间件中的异步操作
// 中间件中可以使用 async/await
export default defineNuxtRouteMiddleware(async (to) => {
const user = await $fetch('/api/me')
if (!user) {
return navigateTo('/login')
}
})INFO
️ 注意:中间件中的异步操作会阻塞导航 如果操作很慢,用户会感觉页面卡住。尽量让中间件逻辑轻量
知识脉络
路由导航 → 你在这里:路由中间件
│
├─→ 下一步:路由守卫(另一种守卫方式)
│
└─→ 相关:页面元信息(meta 传递数据给中间件)