Skip to content

路由守卫

路由守卫允许你在路由变化前后执行逻辑,与中间件类似但使用方式不同。理解守卫和中间件的区别并选择合适的方案,是写出清晰代码的关键。

为什么还需要路由守卫?

路由中间件适合"全局性"的检查(认证、权限),而路由守卫适合"组件级"的逻辑:

场景用中间件用守卫
检查用户是否登录✅ 全局复用❌ 分散到每个页面
表单未保存时阻止离开❌ 中间件不知道表单状态✅ 组件内部逻辑
路由参数变化时刷新数据❌ 中间件不应该处理数据✅ 组件知道如何刷新
记录页面访问日志✅ 全局统一处理也能做,但不集中

简单判断

逻辑是"全局性"的还是"组件级"的?全局性 → 中间件;组件级 → 守卫。

onBeforeRouteLeave

在离开当前页面前触发,最常用的场景是防止用户意外离开未保存的表单

vue
<script setup>
const formChanged = ref(false)

onBeforeRouteLeave((to, from) => {
  if (formChanged.value) {
    const answer = confirm('你有未保存的更改,确定要离开吗?')
    if (!answer) return false  // 阻止导航
  }
})
</script>

返回值说明

返回值效果用途
false阻止导航,用户留在当前页面表单保护
true 或不返回允许导航
路由路径字符串重定向return '/login'
路由对象重定向return { path: '/login', query: { redirect: to.fullPath } }

关键理解

onBeforeRouteLeave 只在客户端执行,不会在 SSR 中运行。因为"离开页面"本身就是客户端行为——SSR 只处理首次进入,不处理页面跳转。

与浏览器 beforeunload 的区别

onBeforeRouteLeave 只能拦截应用内部的导航(NuxtLink、navigateTo 等)。如果用户直接关闭浏览器标签页或输入新 URL,它是拦截不到的:

vue
<script setup>
const formChanged = ref(false)

// ① 拦截应用内部导航(NuxtLink、navigateTo 等)
onBeforeRouteLeave((to, from) => {
  if (formChanged.value) {
    const answer = confirm('你有未保存的更改,确定要离开吗?')
    if (!answer) return false
  }
})

// ② 拦截浏览器关闭/刷新/输入新 URL
onMounted(() => {
  window.addEventListener('beforeunload', (e) => {
    if (formChanged.value) {
      e.preventDefault()  // 浏览器会显示默认的确认对话框
      // 注意:现代浏览器忽略自定义消息,只显示浏览器默认文案
    }
  })
})
</script>

两个守卫缺一不可

  • onBeforeRouteLeave:拦截应用内跳转(如点击导航链接)
  • beforeunload:拦截浏览器级别的离开(关闭标签、刷新、输入新 URL)

如果只想写一个

onBeforeRouteLeave。大多数用户通过应用内导航,很少有人直接改 URL。

实战:完整的表单保护方案

vue
<script setup>
const form = reactive({ title: '', content: '' })
const originalForm = ref('')
const saved = ref(false)

// 判断表单是否有修改
const formChanged = computed(() => {
  return JSON.stringify(form) !== originalForm.value && !saved.value
})

// 保存原始值(在数据加载后)
onMounted(() => {
  originalForm.value = JSON.stringify(form)
})

// 应用内导航保护
onBeforeRouteLeave((to, from) => {
  if (formChanged.value) {
    const answer = confirm('你有未保存的更改,确定要离开吗?')
    if (!answer) return false
  }
})

// 浏览器级别保护
onMounted(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (formChanged.value) {
      e.preventDefault()
    }
  }
  window.addEventListener('beforeunload', handleBeforeUnload)

  onUnmounted(() => {
    window.removeEventListener('beforeunload', handleBeforeUnload)
  })
})

// 保存成功后取消保护
async function save() {
  await $fetch('/api/posts', { method: 'POST', body: form })
  saved.value = true
  originalForm.value = JSON.stringify(form)
}
</script>

onBeforeRouteUpdate

在同一组件复用时(路由参数变化但组件不变)触发:

vue
<script setup>
const route = useRoute()
const { data, refresh } = await useFetch(`/api/users/${route.params.id}`)

onBeforeRouteUpdate(async (to) => {
  // 从 /users/1 导航到 /users/2 时触发
  if (to.params.id !== route.params.id) {
    await refresh()
  }
})
</script>

为什么需要 onBeforeRouteUpdate?

当从 /users/1 导航到 /users/2 时:

text
用户点击 <NuxtLink to="/users/2">


Vue Router 发现两个路由使用同一个组件([id].vue)


为了性能,Vue Router 复用现有组件实例(不销毁、不重建)


onMounted、setup 等不会重新执行 ← 问题:数据不更新!


onBeforeRouteUpdate 触发 ← 在这里处理参数变化

什么时候会组件复用?

当两个路由使用同一个 Vue 组件时。这在动态路由中最常见(如 /users/[id].vue)。

更好的替代方案:useFetch 的 watch

ts
const route = useRoute()
const { data } = await useFetch(`/api/users/${route.params.id}`, {
  watch: [() => route.params.id],  // id 变化时自动刷新
})

推荐用 watch 方案

  • 代码更简洁,不需要手动调用 refresh
  • 响应式追踪更精确,不会遗漏变化
  • onBeforeRouteUpdate 还需要判断参数是否真的变了,watch 自动处理

INFO

onBeforeRouteUpdate 的适用场景:当你需要在参数变化时做一些非数据获取的操作(如重置表单、关闭弹窗、清理状态) 这时 watch 不够灵活,守卫更合适

路由守卫 vs 路由中间件

特性路由守卫路由中间件
定义位置组件内app/middleware/ 目录
作用范围当前组件可复用、可全局
SSR仅客户端SSR + 客户端
可维护性逻辑分散在各组件集中管理
能访问组件状态✅ 可以❌ 不能
适用场景组件级逻辑(表单保护)通用逻辑(认证、权限)

选择原则

  • 需要在 SSR 中执行?→ 中间件
  • 需要在多个页面复用?→ 中间件
  • 只在某个组件内使用?→ 路由守卫
  • 需要访问组件内的状态?→ 路由守卫
  • 涉及浏览器交互(如 confirm)?→ 路由守卫

编程式导航守卫

在插件中设置全局导航守卫:

ts
// app/plugins/router-guards.ts
export default defineNuxtPlugin(() => {
  const router = useRouter()

  // 全局前置守卫
  router.beforeEach((to, from) => {
    console.log(`导航:${from.path} → ${to.path}`)
  })

  // 全局解析守卫
  router.beforeResolve(async (to) => {
    // 在中间件和组件内守卫之后执行
    // 适合确保所有异步操作完成
  })

  // 全局后置钩子
  router.afterEach((to) => {
    // 导航完成后执行,不能阻止
    // 适合日志、统计、页面标题更新
  })
})

三种全局守卫的执行顺序

text
1. beforeEach(全局前置守卫)

2. 路由中间件 + 组件内守卫

3. beforeResolve(全局解析守卫)

4. 导航完成(DOM 更新)

5. afterEach(全局后置钩子)

什么时候用哪种全局守卫?

守卫用途典型场景
beforeEach最早拦截全局认证检查、权限控制
beforeResolve确保异步完成等待所有数据加载后再确认导航
afterEach导航后处理页面统计、标题更新、滚动重置

afterEach 的典型用法

ts
// 页面标题自动更新
router.afterEach((to) => {
  const title = to.meta.title as string
  if (title) {
    useHead({ title: `${title} - 我的网站` })
  }
})
ts
// 页面访问统计
router.afterEach((to) => {
  if (process.client) {
    // 发送页面浏览事件到统计服务
    $fetch('/api/analytics/pageview', {
      method: 'POST',
      body: { path: to.path, title: to.meta.title },
    })
  }
})

INFO

全局守卫 vs 路由中间件:大多数情况下 路由中间件比全局守卫更好用——中间件可以按路由配置(definePageMeta),而全局守卫对所有路由生效。只有在需要"最早/最晚执行"或"全局统一逻辑"时才用全局守卫

异步守卫

守卫可以返回 Promise,支持异步操作:

ts
// 异步 onBeforeRouteLeave
onBeforeRouteLeave(async (to, from) => {
  const hasUnsavedChanges = await checkUnsavedChanges()
  if (hasUnsavedChanges) {
    const answer = confirm('你有未保存的更改,确定要离开吗?')
    if (!answer) return false
  }
})

// 异步全局守卫
router.beforeEach(async (to, from) => {
  const allowed = await $fetch('/api/check-access', {
    query: { path: to.path },
  })
  if (!allowed) {
    return navigateTo('/forbidden')
  }
})

INFO

异步守卫的注意事项

  • 导航会被阻塞,直到 Promise resolve
  • 如果 Promise reject,导航也会被阻止
  • 避免在守卫中做耗时操作(如大量计算、大文件读写),会影响导航体验
  • 如果守卫中有网络请求,添加超时处理

常见问题

守卫不触发

  1. onBeforeRouteLeave:确认是通过 NuxtLink/navigateTo 导航,不是直接改浏览器 URL
  2. onBeforeRouteUpdate:确认是同一个组件复用的场景(路由参数变化)
  3. 全局守卫:确认插件正确注册,且没有语法错误

守卫中用 confirm() 在 SSR 报错

confirm() 是浏览器 API,只在客户端可用。onBeforeRouteLeave 本身只在客户端执行,不会有问题。但如果在 beforeEach 等全局守卫中使用,需要判断环境:

ts
router.beforeEach((to) => {
  if (process.client) {
    // 只在客户端使用浏览器 API
  }
})

中间件和守卫同时存在时的执行顺序

text
1. beforeEach(全局前置守卫)
2. 页面中间件(definePageMeta 中定义的)
3. onBeforeRouteLeave / onBeforeRouteUpdate(组件内守卫)
4. beforeResolve(全局解析守卫)
5. 导航完成
6. afterEach(全局后置钩子)

知识脉络

text
路由中间件 → 你在这里:路由守卫

               ├─→ 下一步:路由规则

               ├─→ 相关:页面元信息(definePageMeta 中注册中间件)

               └─→ 相关:路由导航(navigateTo 的使用)

基于 Nuxt 4 官方文档整理编写