路由中间件体系

路由中间件是 Nuxt4 路由系统的"安检站"——每次路由切换时,中间件先于页面组件执行,决定这次导航放行、重定向还是中止。本章从全局/局部中间件的执行机制讲起,深入 defineNuxtRouteMiddleware API、服务端与客户端执行差异,最终用 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 / 刷新)服务端
客户端导航(&lt;NuxtLink&gt;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, &#123; replace: true &#125;)重定向并替换历史记录
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(&#123; middleware &#125;) 引用
  • 执行顺序:全局(按文件名排序) → 页面指定(按数组顺序)
  • SSR/CSR 差异:首次访问在服务端执行,客户端导航在浏览器执行;用 useCookie 统一两端
  • navigateTo:重定向,支持路径/对象/外部链接/replace/301
  • abortNavigation:中止导航,可带错误信息
  • 实战方案:auth-init 全局初始化 → auth/admin/member 局部拦截 → 登录回跳

下一章探索路由过渡动画与 View Transitions API。