导航守卫是 Vue Router 提供的钩子函数,用于在路由导航过程中进行拦截和控制。它们允许我们在路由跳转前、跳转后或跳转失败时执行自定义逻辑,是实现权限控制、数据预加载等功能的关键。
导航守卫主要分为三类:
导航守卫的本质是在路由导航的不同阶段插入钩子函数,通过这些钩子函数可以:
当触发路由导航时,导航守卫会按照特定顺序执行:
用户点击链接或调用 router.push()
beforeRouteLeave(组件内守卫)
beforeEach 守卫全局前置守卫
beforeRouteUpdate 守卫组件内守卫(仅当组件复用时)
beforeEnter 守卫路由独享守卫
加载异步组件
beforeRouteEnter 守卫组件内守卫
beforeResolve 守卫全局解析守卫
所有守卫通过,开始导航
afterEach 钩子全局后置钩子
组件渲染完成
beforeRouteEnter 中传给 next 的回调可以访问组件实例
全局守卫作用于所有路由导航,是最常用的守卫类型。
beforeEach在路由跳转前执行,常用于权限验证。
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
// 全局前置守卫
router.beforeEach((to, from, next) => {
// to: 即将要进入的目标路由对象
// from: 当前导航正要离开的路由对象
// next: 回调函数,必须调用以继续或中断导航
console.log(`从 ${from.path} 导航到 ${to.path}`)
// 1. 继续导航
next()
// 2. 中断导航,停留在当前页面
// next(false)
// 3. 跳转到指定路由
// next('/login')
// next({ path: '/login' })
// next({ name: 'login', query: { redirect: to.fullPath } })
// 4. 抛出错误,取消导航并调用 onError 回调
// next(new Error('导航被拒绝'))
})
export default router
// 模拟用户认证状态
let isAuthenticated = false
let userRole = null
// 登录函数
function login(role = 'user') {
isAuthenticated = true
userRole = role
}
// 登出函数
function logout() {
isAuthenticated = false
userRole = null
}
// 权限检查函数
function checkPermission(requiredRole) {
if (!requiredRole) return true
if (!userRole) return false
// 简单的角色权限检查
const roleHierarchy = {
'user': 1,
'editor': 2,
'admin': 3
}
return roleHierarchy[userRole] >= roleHierarchy[requiredRole]
}
// 全局前置守卫 - 权限控制
router.beforeEach((to, from, next) => {
// 检查路由是否需要认证
if (to.meta.requiresAuth && !isAuthenticated) {
// 保存用户想要访问的地址,登录后重定向回来
next({
name: 'login',
query: { redirect: to.fullPath }
})
return
}
// 检查路由角色权限
if (to.meta.roles && !checkPermission(to.meta.roles)) {
// 权限不足,重定向到无权限页面
next({ name: 'forbidden' })
return
}
// 检查是否只允许未登录用户访问(如登录页)
if (to.meta.guestOnly && isAuthenticated) {
// 已登录用户访问登录页,重定向到首页
next({ name: 'home' })
return
}
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 我的应用`
}
// 所有检查通过,继续导航
next()
})
beforeResolve在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用。
router.beforeResolve((to, from) => {
// 在导航被确认之前调用
// 此时组件已经解析完成
// 检查页面是否需要某些数据
if (to.meta.requiresData) {
// 可以在这里进行数据预加载
return loadRequiredData(to.params.id)
.then(() => {
// 数据加载成功,导航继续
console.log('数据预加载完成')
})
.catch(error => {
// 数据加载失败,取消导航
console.error('数据加载失败:', error)
return false
})
}
})
// 辅助函数:加载数据
async function loadRequiredData(id) {
// 模拟异步数据加载
return new Promise((resolve) => {
setTimeout(() => {
console.log(`已加载数据: ${id}`)
resolve()
}, 100)
})
}
afterEach在导航完成后调用,没有 next 参数,不能改变导航。
router.afterEach((to, from, failure) => {
// to: 进入的目标路由
// from: 离开的路由
// failure: 导航失败的错误信息(如果有)
if (failure) {
console.error('导航失败:', failure)
// 可以在这里记录错误或显示错误信息
showErrorMessage(failure.message)
return
}
// 导航成功后的操作
console.log(`成功从 ${from.path} 导航到 ${to.path}`)
// 1. 滚动到页面顶部
window.scrollTo(0, 0)
// 2. 发送页面浏览统计
sendPageView(to.path)
// 3. 关闭加载提示
hideLoading()
// 4. 更新面包屑导航
updateBreadcrumb(to.matched)
})
// 辅助函数示例
function sendPageView(path) {
// 发送页面浏览统计到分析服务
if (window.ga) {
window.ga('send', 'pageview', path)
}
}
function updateBreadcrumb(matchedRoutes) {
// 根据匹配的路由更新面包屑
const breadcrumbs = matchedRoutes.map(route => ({
name: route.meta.breadcrumb || route.name,
path: route.path
}))
// 更新面包屑组件
// ...
}
beforeEnter在路由配置上直接定义的守卫,只对该路由生效。
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: { title: '首页' }
},
{
path: '/dashboard',
name: 'dashboard',
component: Dashboard,
meta: {
title: '控制面板',
requiresAuth: true,
roles: ['admin', 'editor']
},
// 路由独享守卫
beforeEnter: (to, from, next) => {
console.log('进入控制面板前检查')
// 检查用户是否已登录
if (!isUserLoggedIn()) {
next({ name: 'login' })
return
}
// 检查用户是否有权限
if (!hasPermission('dashboard')) {
next({ name: 'forbidden' })
return
}
// 检查是否需要完成引导
if (needsOnboarding()) {
next({ name: 'onboarding' })
return
}
// 所有检查通过
next()
}
},
{
path: '/user/:id',
name: 'user',
component: UserProfile,
// 支持多个守卫函数
beforeEnter: [
// 第一个守卫:验证参数
(to, from, next) => {
const id = to.params.id
if (!isValidUserId(id)) {
next({ name: 'not-found' })
return
}
next()
},
// 第二个守卫:检查权限
(to, from, next) => {
if (!canViewUser(to.params.id)) {
next({ name: 'forbidden' })
return
}
next()
}
]
},
{
path: '/admin',
name: 'admin',
component: Admin,
meta: { roles: ['admin'] },
// 异步守卫
beforeEnter: async (to, from, next) => {
try {
// 异步检查权限
const hasAccess = await checkAdminAccess()
if (hasAccess) {
next()
} else {
next({ name: 'forbidden' })
}
} catch (error) {
console.error('权限检查失败:', error)
next({ name: 'error', query: { code: '403' } })
}
}
}
]
在组件内部定义的守卫,只对该组件生效。
beforeRouteEnter在渲染该组件的对应路由被验证前调用,不能访问 this。
<!-- UserProfile.vue -->
<template>
<div>
<h2>用户资料</h2>
<p>用户名: {{ user.name }}</p>
</div>
</template>
<script>
// Options API
export default {
name: 'UserProfile',
data() {
return {
user: {}
}
},
// 组件内守卫 - 进入前
beforeRouteEnter(to, from, next) {
// 不能访问 this,因为组件实例还没创建
console.log('准备进入用户资料页面')
// 1. 验证用户ID
const userId = to.params.id
if (!userId || userId === '0') {
next({ name: 'not-found' })
return
}
// 2. 预加载用户数据
fetchUserData(userId)
.then(user => {
// 通过 next 的回调访问组件实例
next(vm => {
// vm 是组件实例
vm.user = user
console.log('用户数据已设置:', user.name)
})
})
.catch(error => {
console.error('加载用户数据失败:', error)
// 重定向到错误页面
next({ name: 'error', query: { message: '用户不存在' } })
})
}
}
</script>
<!-- Composition API -->
<script setup>
import { onBeforeRouteEnter } from 'vue-router'
// 使用 onBeforeRouteEnter
onBeforeRouteEnter((to, from, next) => {
console.log('准备进入用户资料页面 (Composition API)')
// 预加载数据
fetchUserData(to.params.id)
.then(user => {
next(vm => {
// vm 是组件实例
// 可以通过 vm.setupState 访问 Composition API 的状态
console.log('组件实例:', vm)
})
})
.catch(error => {
next({ name: 'error' })
})
})
</script>
beforeRouteUpdate在当前路由改变,但是该组件被复用时调用。
<!-- UserProfile.vue -->
<template>
<div>
<h2>用户资料</h2>
<p>用户ID: {{ userId }}</p>
<p>用户名: {{ userName }}</p>
</div>
</template>
<script>
// Options API
export default {
name: 'UserProfile',
data() {
return {
userId: null,
userName: ''
}
},
// 组件内守卫 - 路由更新(组件复用时)
beforeRouteUpdate(to, from, next) {
// 可以访问 this
console.log(`用户ID从 ${from.params.id} 变为 ${to.params.id}`)
// 当用户ID变化时,重新加载数据
if (to.params.id !== from.params.id) {
this.loadUserData(to.params.id)
.then(() => {
next()
})
.catch(error => {
console.error('重新加载用户数据失败:', error)
// 可以取消导航或重定向
next(false)
})
} else {
// 参数未变化,直接继续
next()
}
},
methods: {
async loadUserData(userId) {
// 加载用户数据
const user = await fetchUserData(userId)
this.userId = user.id
this.userName = user.name
}
}
}
</script>
<!-- Composition API -->
<script setup>
import { ref, watch } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
const route = useRoute()
const userId = ref(route.params.id)
const userName = ref('')
// 使用 onBeforeRouteUpdate
onBeforeRouteUpdate((to, from, next) => {
console.log('路由更新 (Composition API)')
// 当用户ID变化时重新加载数据
if (to.params.id !== from.params.id) {
loadUserData(to.params.id)
.then(() => next())
.catch(() => next(false))
} else {
next()
}
})
// 或者使用 watch 监听路由参数变化
watch(
() => route.params.id,
(newId) => {
if (newId) {
loadUserData(newId)
}
}
)
</script>
beforeRouteLeave在导航离开该组件的对应路由时调用。
<!-- Editor.vue -->
<template>
<div>
<h2>编辑器</h2>
<textarea v-model="content" rows="10" cols="50"></textarea>
<button @click="save">保存</button>
</div>
</template>
<script>
// Options API
export default {
name: 'Editor',
data() {
return {
content: '',
hasUnsavedChanges: false
}
},
watch: {
content() {
this.hasUnsavedChanges = true
}
},
// 组件内守卫 - 离开前
beforeRouteLeave(to, from, next) {
// 可以访问 this
console.log('准备离开编辑器')
// 检查是否有未保存的更改
if (this.hasUnsavedChanges) {
// 显示确认对话框
const answer = window.confirm(
'您有未保存的更改。确定要离开吗?'
)
if (answer) {
// 用户确认离开
next()
} else {
// 取消导航
next(false)
}
} else {
// 没有未保存的更改,直接离开
next()
}
},
methods: {
save() {
// 保存内容
saveContent(this.content)
.then(() => {
this.hasUnsavedChanges = false
alert('保存成功!')
})
.catch(error => {
alert('保存失败: ' + error.message)
})
}
}
}
</script>
<!-- Composition API -->
<script setup>
import { ref, watch } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
const content = ref('')
const hasUnsavedChanges = ref(false)
// 监听内容变化
watch(content, () => {
hasUnsavedChanges.value = true
})
// 使用 onBeforeRouteLeave
onBeforeRouteLeave((to, from, next) => {
console.log('准备离开编辑器 (Composition API)')
if (hasUnsavedChanges.value) {
if (confirm('您有未保存的更改。确定要离开吗?')) {
next()
} else {
next(false)
}
} else {
next()
}
})
</script>
router.beforeEach((to, from, next) => {
// 常用属性示例:
// 1. 路径相关
console.log('完整路径:', to.fullPath) // "/user/123?name=张三#profile"
console.log('路径:', to.path) // "/user/123"
console.log('哈希:', to.hash) // "#profile"
// 2. 参数相关
console.log('路径参数:', to.params) // { id: "123" }
console.log('查询参数:', to.query) // { name: "张三" }
// 3. 路由信息
console.log('路由名称:', to.name) // "user"
console.log('路由元信息:', to.meta) // { requiresAuth: true }
// 4. 匹配的路由记录
console.log('匹配的路由记录:', to.matched)
// 数组,包含当前路由和所有父级路由
// 5. 重定向来源
console.log('重定向来源:', to.redirectedFrom)
// 如果当前导航是通过重定向过来的,这里会有重定向来源的路由
// 6. from 对象的属性与 to 相同
console.log('离开的页面标题:', from.meta?.title)
next()
})
router.beforeEach((to, from, next) => {
// next() 的几种调用方式:
// 1. 继续导航(无参数)
// next()
// 2. 中断导航,停留在当前页面
// next(false)
// 3. 跳转到指定路径
// next('/login')
// next({ path: '/login' })
// 4. 跳转到命名路由
// next({ name: 'login' })
// next({ name: 'user', params: { id: '123' } })
// 5. 跳转并携带查询参数
// next({
// path: '/search',
// query: { q: 'vue', page: 1 }
// })
// 6. 替换当前路由(不添加历史记录)
// next({
// path: '/new-path',
// replace: true
// })
// 7. 抛出错误
// next(new Error('导航被拒绝'))
// 错误会被传递给 router.onError() 回调
// 8. beforeRouteEnter 特有的回调形式
// next(vm => {
// // 在组件实例创建后执行
// vm.loadData()
// })
})
点击按钮模拟路由导航,观察控制台输出:
// 权限控制工具函数
const auth = {
// 检查登录状态
isAuthenticated() {
return localStorage.getItem('token') !== null
},
// 获取用户角色
getUserRole() {
const user = JSON.parse(localStorage.getItem('user') || '{}')
return user.role || null
},
// 检查权限
hasPermission(requiredRole) {
const userRole = this.getUserRole()
if (!requiredRole) return true
if (!userRole) return false
// 角色权限映射
const rolePermissions = {
'user': ['dashboard', 'profile'],
'editor': ['dashboard', 'profile', 'editor'],
'admin': ['dashboard', 'profile', 'editor', 'admin']
}
return rolePermissions[userRole]?.includes(requiredRole) || false
},
// 检查路由权限
canAccessRoute(route) {
const meta = route.meta || {}
// 检查是否需要登录
if (meta.requiresAuth && !this.isAuthenticated()) {
return { allowed: false, redirect: '/login' }
}
// 检查角色权限
if (meta.roles && !this.hasPermission(meta.roles)) {
return { allowed: false, redirect: '/forbidden' }
}
// 检查是否只允许未登录用户访问
if (meta.guestOnly && this.isAuthenticated()) {
return { allowed: false, redirect: '/' }
}
return { allowed: true }
}
}
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
console.log(`导航守卫: ${from.path} → ${to.path}`)
// 检查路由权限
const result = auth.canAccessRoute(to)
if (!result.allowed) {
// 权限不足,重定向
next({
path: result.redirect,
query: {
redirect: to.fullPath,
reason: result.reason
}
})
return
}
// 页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 我的应用`
}
// 检查是否需要预加载数据
if (to.meta.preload) {
try {
await preloadData(to)
next()
} catch (error) {
console.error('数据预加载失败:', error)
next({ name: 'error', query: { code: 'LOAD_ERROR' } })
}
} else {
next()
}
})
// 全局解析守卫 - 数据预取
router.beforeResolve(async (to, from) => {
if (to.meta.dataLoader) {
return to.meta.dataLoader(to.params)
}
})
// 全局后置钩子 - 页面统计
router.afterEach((to, from, failure) => {
if (failure) {
console.error('导航失败:', failure)
trackError(failure)
return
}
// 页面浏览统计
trackPageView(to.path, to.name)
// 滚动到顶部
window.scrollTo(0, 0)
})
// 路由配置示例
const routes = [
{
path: '/login',
name: 'login',
component: () => import('./views/Login.vue'),
meta: {
title: '登录',
guestOnly: true
}
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('./views/Dashboard.vue'),
meta: {
title: '控制面板',
requiresAuth: true,
preload: true
},
beforeEnter: (to, from, next) => {
// 路由独享守卫:额外检查
console.log('进入控制面板前额外检查')
next()
}
},
{
path: '/admin',
name: 'admin',
component: () => import('./views/Admin.vue'),
meta: {
title: '管理后台',
requiresAuth: true,
roles: 'admin',
dataLoader: loadAdminData
}
}
]
// 数据加载函数
async function loadAdminData(params) {
// 加载管理后台所需数据
const [users, stats, logs] = await Promise.all([
fetch('/api/admin/users'),
fetch('/api/admin/stats'),
fetch('/api/admin/logs')
])
return { users, stats, logs }
}
在守卫中返回 Promise 或使用 async/await。Vue Router 会等待 Promise 解析后再继续导航。
在重定向时检查目标路由,避免重定向到当前路由或产生循环。可以使用 to.redirectedFrom 检查重定向来源。
beforeRouteEnter 中如何访问组件实例?通过 next 的回调函数:next(vm => { /* 访问 vm */ })。回调在组件创建后执行。
调用 next(false) 取消导航。可以在 afterEach 中通过 failure 参数检测导航是否失败。
可以使用路由元信息标记,在全局守卫中检查并跳过:
if (to.meta.skipGuards) {
next()
return
}
Vue Router 的导航守卫提供了强大的路由控制能力,允许我们在路由导航的各个阶段插入自定义逻辑。通过合理使用导航守卫,我们可以:
理解不同守卫的执行顺序和适用场景,结合最佳实践,可以构建出安全、稳定、用户体验良好的单页面应用。
beforeRouteEnter 中如何访问组件实例?为什么不能直接使用 this?