Nuxt: 中间件
最后更新:2026-08-26
Bob 发现 Alice 能直接访问 /admin 管理后台,毫无权限检查。Charlie 需要一个路由守卫——未登录用户访问管理页面自动跳转到登录页,普通用户不能访问管理员路由。Nuxt 3 的中间件系统就是解决方案。
1. 你将学到
- 全局中间件:middleware/auth.global.ts 全局生效与执行顺序
- 命名中间件:definePageMeta + middleware 数组组合
- 内联中间件:页面内直接定义的轻量中间件
- 中间件与 SSR:双重执行机制与纯客户端中间件
- MegaShop 管理员路由守卫 + 角色检查实战
2. 一个管理员的真实故事
(1) 痛点:无权限控制的致命漏洞
Alice 在浏览器输入 /admin 就进了 MegaShop 管理后台,能看到所有订单和用户数据。Bob 作为管理员,发现自己和普通用户看到的界面完全一样,没有任何区分。Charlie 紧急需要路由级别的权限守卫。
(2) 路由中间件的解法
Nuxt 3 中间件在路由跳转前执行,可以重定向或阻止访问:
TYPESCRIPT
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
if (!isAuthenticated()) return navigateTo('/login')
})
(3) 收益:路由级权限控制
Alice 再也无法直接访问 /admin,未登录自动跳转登录页。Bob 的管理员权限在路由层面得到保障。
3. 三种中间件模式
(1) 中间件执行顺序
flowchart LR
A[Route Navigation] --> B[Global Middleware 1]
B --> C[Global Middleware 2]
C --> D[Named/Inline Middleware]
D --> E[Page Render]
D -->|abort| F[Redirect / Abort]
B -->|abort| F
C -->|abort| F
(2) 三种模式对比
| 维度 | 全局中间件 | 命名中间件 | 内联中间件 |
|---|---|---|---|
| 文件名 | *.global.ts | *.ts(无 global) | 无文件 |
| 生效范围 | 所有路由 | 指定页面 | 当前页面 |
| 定义方式 | middleware/ 目录 | middleware/ 目录 | definePageMeta 内 |
| 复用性 | ✅ 自动复用 | ✅ 多页引用 | ❌ 仅当前页 |
| 适用场景 | 全局鉴权/日志 | 特定页面守卫 | 一次性简单检查 |
4. 全局中间件
▶ 示例:全局鉴权中间件
TYPESCRIPT
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Skip auth for public pages
const publicPages = ['/', '/products', '/about', '/login', '/register']
if (publicPages.includes(to.path)) return
const token = useCookie('auth-token')
if (!token.value) {
return navigateTo(`/login?redirect=${to.path}`, { redirectCode: 302 })
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:全局日志中间件
TYPESCRIPT
// middleware/log.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Navigation: ${from.path} → ${to.path}`)
// No return = allow navigation
})
输出:
TEXT
📖 仅展示
// 执行成功
⚠️ 注意: 全局中间件按文件名字母顺序执行。
01-auth.global.ts 先于 02-log.global.ts。建议用数字前缀控制顺序。
5. 命名中间件
▶ 示例:管理员角色中间件
TYPESCRIPT
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const user = useState('user')
if (!user.value) {
return navigateTo(`/login?redirect=${to.path}`)
}
if (user.value.role !== 'admin') {
// Regular user trying to access admin page
throw createError({
statusCode: 403,
message: 'Access denied. Admin role required.'
})
}
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:页面引用命名中间件
VUE
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>Admin Dashboard</h1>
<p>Welcome, Bob (Admin)</p>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'admin', // Reference named middleware
layout: 'sidebar'
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:组合多个中间件
VUE
<!-- pages/admin/settings.vue -->
<template>
<div>
<h1>Admin Settings</h1>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'], // Execute in order: auth → admin
layout: 'sidebar'
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
6. 内联中间件
▶ 示例:页面内定义中间件
VUE
<!-- pages/checkout.vue -->
<template>
<div>
<h1>Checkout</h1>
<p>Cart total: ${{ total }} USD</p>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: defineNuxtRouteMiddleware((to) => {
// Inline: check if cart has items
const cart = useState<any[]>('cart')
if (cart.value.length === 0) {
return navigateTo('/cart')
}
})
})
const cart = useState<any[]>('cart')
const total = computed(() => cart.value.reduce((s, i) => s + i.price, 0))
</script>
输出:
TEXT
📖 仅展示
// 执行成功
(1) 内联 vs 命名中间件选择
| 场景 | 选择 | 理由 |
|---|---|---|
| 全站鉴权 | 全局 | 每个路由都要检查 |
| 管理员路由 | 命名 | 多个 admin 页面共享 |
| 购物车非空检查 | 内联 | 只在 checkout 页用 |
| VIP 专区 | 命名 | 可能在多个页面使用 |
| A/B 测试分流 | 内联 | 一次性逻辑 |
7. 中间件与 SSR
(1) SSR 双重执行机制
flowchart TB
A[User navigates to /admin] --> B[Server: middleware executes]
B -->|redirect| C[Server sends redirect response]
B -->|allow| D[Server renders page]
D --> E[Client: middleware executes again]
E -->|redirect| F[Client-side redirect]
E -->|allow| G[Client hydrates page]
▶ 示例:纯客户端中间件
TYPESCRIPT
// middleware/client-only.ts
export default defineNuxtRouteMiddleware((to) => {
// Skip on server - only check on client
if (import.meta.server) return
const localStorage = window.localStorage
const preferences = JSON.parse(localStorage.getItem('user-preferences') || '{}')
if (preferences.theme === 'dark') {
// Client-only logic using browser APIs
document.documentElement.classList.add('dark')
}
})
输出:
TEXT
📖 仅展示
// 执行成功
(2) SSR 中间件注意事项
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 中间件执行两次 | SSR + 客户端各一次 | 正常行为,确保幂等 |
| 使用 window/localStorage | SSR 无浏览器环境 | if (import.meta.client) 判断 |
| Cookie 在 SSR 可用 | 请求携带 Cookie | 用 useCookie 读取 |
| navigateTo 在 SSR | 返回 302 重定向 | 正常行为 |
| useState 在中间件 | ✅ 可用 | 适合检查登录状态 |
8. 综合示例:MegaShop 权限守卫体系
TYPESCRIPT
// middleware/01-auth.global.ts - Global auth check
export default defineNuxtRouteMiddleware((to) => {
const publicPaths = ['/', '/products', '/products/**', '/about', '/login', '/register']
const isPublic = publicPaths.some(path => {
if (path.includes('**')) return to.path.startsWith(path.replace('/**', ''))
return path === to.path
})
if (isPublic) return
const token = useCookie('auth-token')
if (!token.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.path)}`)
}
})
TYPESCRIPT
// middleware/admin.ts - Admin role check
export default defineNuxtRouteMiddleware((to) => {
const user = useCookie('user-data')
if (!user.value) {
return navigateTo('/login')
}
if ((user.value as any).role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
})
VUE
<!-- pages/admin/products/index.vue -->
<template>
<div>
<h1>Product Management</h1>
<p>Manage 1 million products</p>
<!-- Admin product CRUD interface -->
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['admin'],
layout: 'sidebar'
})
</script>
❓ 常见问题
Q 中间件执行顺序怎么控制?
A 全局中间件按文件名字母排序,建议用数字前缀(01-xxx.global.ts)。命名中间件按 definePageMeta 中的数组顺序执行。
Q 中间件里能做异步操作吗?
A defineNuxtRouteMiddleware 本身不支持 async。但可以在中间件内调用同步 API(如 useCookie/useState)。需要异步验证时,建议在插件或页面 setup 中处理。
Q 为什么中间件在 SSR 和客户端各执行一次?
A SSR 时中间件检查路由权限决定是否渲染,客户端水合时再次执行确保一致性。两次执行的逻辑应该幂等。
Q 全局中间件对性能有影响吗?
A 每次路由跳转都会执行所有全局中间件。保持中间件逻辑轻量,避免在全局中间件中做重计算或 API 调用。
Q 中间件里抛出 createError 会怎样?
A SSR 时返回对应状态码的 HTML 错误页(如 403 页面)。客户端时显示 Nuxt 错误页面。可以自定义 error.vue。
Q 如何在中间件间传递数据?
A 用 useState 或 NuxtApp 的 payload。
const nuxtApp = useNuxtApp() → nuxtApp.payload.xxx = data。下一个中间件或页面可以读取。📖 小节
- 三种中间件:全局(所有路由)、命名(指定页面)、内联(当前页面)
- 全局中间件按文件名排序执行,建议用数字前缀控制顺序
- 命名中间件通过 definePageMeta 的 middleware 数组引用,可组合多个
- SSR 双重执行是正常行为,浏览器 API 需用 import.meta.client 保护
- MegaShop 用全局 auth + 命名 admin 实现完整权限守卫
📝 作业
- 基础题(难度⭐):创建一个命名中间件 guest.ts,只允许未登录用户访问(已登录跳转首页),应用到 /login 页面
- 进阶题(难度⭐⭐):实现完整的权限体系:全局 auth 检查登录 + 命名 admin 检查角色,/admin/** 路由受保护
- 挑战题(难度⭐⭐⭐):实现中间件间的数据传递——在 auth 中间件获取用户信息存入 NuxtApp payload,在 admin 中间件直接读取,避免重复请求
---|