路由中间件体系
路由中间件是 Nuxt4 路由系统的"安检站"——每次路由切换时,中间件先于页面组件执行,决定这次导航放行、重定向还是中止。本章从全局/局部中间件的执行机制讲起,深入
defineNuxtRouteMiddlewareAPI、服务端与客户端执行差异,最终用 AI 视频项目的权限拦截作为完整实战案例。
1. 全局中间件 vs 局部中间件
1.1 两种中间件的对比
| 特性 | 全局中间件 | 局部(命名)中间件 |
|---|---|---|
| 文件命名 | *.global.ts | *.ts(无 .global 后缀) |
| 触发方式 | 每次路由切换自动执行 | 页面通过 definePageMeta 显式引用 |
| 典型场景 | 日志、分析埋点、全局认证检查 | 特定页面的权限校验 |
| 存放目录 | app/middleware/ | app/middleware/ |
1.2 目录结构
app/middleware/
├── analytics.global.ts # 全局:每次导航都记录埋点
├── auth-check.global.ts # 全局:检查 Token 是否过期
├── auth.ts # 局部:需要登录的页面引用
├── admin.ts # 局部:需要管理员权限的页面引用
└── member.ts # 局部:需要会员身份的页面引用
1.3 执行顺序
路由切换触发
│
▼
① 全局中间件(按文件名字母顺序)
analytics.global.ts → auth-check.global.ts
│
▼
② 页面指定的局部中间件(按数组顺序)
definePageMeta({ middleware: ['auth', 'admin'] })
auth.ts → admin.ts
│
▼
③ 页面组件渲染
如果需要控制全局中间件的执行顺序,可以用数字前缀命名:
app/middleware/
├── 01.logger.global.ts # 先执行
├── 02.auth-check.global.ts # 后执行
└── auth.ts
2. defineNuxtRouteMiddleware 用法
2.1 基本语法
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
// to — 目标路由对象
// from — 来源路由对象
// 返回值决定导航行为:
// undefined / void → 放行(继续导航)
// navigateTo(...) → 重定向
// abortNavigation() → 中止导航
})2.2 局部中间件示例
// app/middleware/auth.ts — 需要登录才能访问
export default defineNuxtRouteMiddleware((to) => {
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) {
// 保存原始目标路径,登录后回跳
return navigateTo({
path: '/login',
query: { redirect: to.fullPath },
})
}
// 已登录 → 不返回任何值,放行
})在页面中引用:
<!-- app/pages/studio/index.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth', // 访问工作台需要先登录
})
</script>2.3 全局中间件示例
// app/middleware/analytics.global.ts — 页面访问埋点
export default defineNuxtRouteMiddleware((to, from) => {
// 仅在客户端执行
if (import.meta.server) return
// 记录页面访问
if (from.path !== to.path) {
trackPageView({
path: to.path,
referrer: from.path,
timestamp: Date.now(),
})
}
})2.4 多个中间件串联
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'],
// 先执行 auth(检查登录),再执行 admin(检查管理员权限)
// 如果 auth 返回了重定向,admin 不会执行
})
</script>2.5 内联中间件
对于只有一个页面使用的简单逻辑,可以直接内联:
<script setup lang="ts">
definePageMeta({
middleware: (to) => {
// 维护模式:所有页面重定向到维护页
const config = useRuntimeConfig()
if (config.public.maintenanceMode) {
return navigateTo('/maintenance')
}
},
})
</script>3. 服务端与客户端执行差异
3.1 执行时机
路由中间件的执行环境取决于导航方式:
| 导航方式 | 执行环境 |
|---|---|
| 首次访问(直接输入 URL / 刷新) | 服务端 |
客户端导航(<NuxtLink>、navigateTo) | 客户端 |
3.2 环境检测
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
// 方式 1:import.meta 检测
if (import.meta.server) {
// 服务端逻辑(SSR 首次渲染时执行)
console.log('[Server] Middleware executing for:', to.path)
}
if (import.meta.client) {
// 客户端逻辑(SPA 导航时执行)
console.log('[Client] Middleware executing for:', to.path)
}
// 方式 2:useRequestEvent(仅服务端可用)
const event = useRequestEvent()
if (event) {
// 服务端:可以访问请求头、Cookie 等
const token = getCookie(event, 'auth-token')
if (!token) return navigateTo('/login')
} else {
// 客户端:从 Cookie 或 Store 获取认证状态
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) return navigateTo('/login')
}
})3.3 常见陷阱
// ❌ 错误:在中间件中访问浏览器 API
export default defineNuxtRouteMiddleware(() => {
const token = localStorage.getItem('token') // 💥 SSR 中没有 localStorage
})
// ✅ 正确:使用 useCookie(SSR/CSR 通用)
export default defineNuxtRouteMiddleware(() => {
const token = useCookie('auth-token')
if (!token.value) return navigateTo('/login')
})useCookie 是 Nuxt4 的杀手级 API——在 SSR 时从 Cookie 请求头读取,在 CSR 时从 document.cookie 读取,同一份代码两端通用。
4. navigateTo 与 abortNavigation
4.1 navigateTo — 重定向
// 字符串路径
return navigateTo('/login')
// 路由对象(带参数)
return navigateTo({
path: '/login',
query: { redirect: to.fullPath },
})
// 命名路由
return navigateTo({
name: 'videos-id',
params: { id: '123' },
})
// 外部链接
return navigateTo('https://example.com', { external: true })
// 替换当前历史记录(不产生后退记录)
return navigateTo('/login', { replace: true })
// 301 重定向(SSR 时发送 HTTP 301,利于 SEO)
return navigateTo('/new-path', { redirectCode: 301 })4.2 abortNavigation — 中止导航
// 静默中止(停留在当前页面,不报错)
return abortNavigation()
// 带错误信息中止(触发错误页面)
return abortNavigation(
createError({
statusCode: 403,
statusMessage: '无权访问',
})
)4.3 返回值语义总结
| 返回值 | 行为 |
|---|---|
undefined / 不返回 | 放行,继续导航 |
navigateTo(path) | 重定向到指定路径 |
navigateTo(path, { replace: true }) | 重定向并替换历史记录 |
abortNavigation() | 中止导航,停留在当前页 |
abortNavigation(error) | 中止并显示错误页面 |
5. AI 视频项目权限拦截实战
5.1 权限模型设计
// shared/types/user.ts
export type UserRole = 'visitor' | 'user' | 'member' | 'admin'
export interface User {
id: string
name: string
email: string
role: UserRole
memberExpiredAt?: string // 会员到期时间
}5.2 认证状态 Composable
// app/composables/useAuth.ts
export function useAuth() {
const user = useState<User | null>('auth:user', () => null)
const token = useCookie('auth-token', { maxAge: 60 * 60 * 24 * 7 })
const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin')
const isMember = computed(() => {
if (!user.value?.memberExpiredAt) return false
return new Date(user.value.memberExpiredAt) > new Date()
})
async function fetchUser() {
if (!token.value) return
try {
user.value = await $fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token.value}` },
})
} catch {
token.value = null
user.value = null
}
}
function logout() {
token.value = null
user.value = null
navigateTo('/login')
}
return { user, token, isLoggedIn, isAdmin, isMember, fetchUser, logout }
}5.3 全局认证初始化中间件
// app/middleware/01.auth-init.global.ts
export default defineNuxtRouteMiddleware(async () => {
const { user, token, fetchUser } = useAuth()
// 有 Token 但没有用户信息时,从 API 获取
if (token.value && !user.value) {
await fetchUser()
}
})5.4 登录拦截中间件
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) {
return navigateTo({
path: '/login',
query: { redirect: to.fullPath },
})
}
})5.5 管理员权限中间件
// app/middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
const { isLoggedIn, isAdmin } = useAuth()
if (!isLoggedIn.value) {
return navigateTo('/login')
}
if (!isAdmin.value) {
return abortNavigation(
createError({
statusCode: 403,
statusMessage: '仅管理员可访问',
})
)
}
})5.6 会员校验中间件
// app/middleware/member.ts
export default defineNuxtRouteMiddleware(() => {
const { isLoggedIn, isMember } = useAuth()
if (!isLoggedIn.value) {
return navigateTo('/login')
}
if (!isMember.value) {
return navigateTo('/pricing') // 引导开通会员
}
})5.7 页面中使用
<!-- app/pages/studio/create.vue — 需要登录 -->
<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
</script>
<!-- app/pages/studio/ai-generate.vue — 需要会员 -->
<script setup lang="ts">
definePageMeta({ middleware: ['auth', 'member'] })
</script>
<!-- app/pages/admin/users.vue — 需要管理员 -->
<script setup lang="ts">
definePageMeta({ middleware: 'admin' })
</script>5.8 登录后回跳
<!-- app/pages/(auth)/login.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'auth' })
const route = useRoute()
async function handleLogin(credentials: LoginInput) {
const { token, fetchUser } = useAuth()
const data = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials,
})
token.value = data.token
await fetchUser()
// 登录成功后回跳到原始页面
const redirect = route.query.redirect as string
navigateTo(redirect || '/')
}
</script>本章小结
- 全局中间件(
.global.ts):每次导航自动执行,适合埋点、认证初始化 - 局部中间件:页面通过
definePageMeta({ middleware })引用 - 执行顺序:全局(按文件名排序) → 页面指定(按数组顺序)
- SSR/CSR 差异:首次访问在服务端执行,客户端导航在浏览器执行;用
useCookie统一两端 - navigateTo:重定向,支持路径/对象/外部链接/replace/301
- abortNavigation:中止导航,可带错误信息
- 实战方案:auth-init 全局初始化 → auth/admin/member 局部拦截 → 登录回跳
下一章探索路由过渡动画与 View Transitions API。