路由守卫用于在路由跳转过程中执行特定逻辑,如权限验证、数据预取、页面标题设置等。理解路由守卫,是构建安全、可控应用的关键。
在路由跳转前触发,常用于权限验证:
const router = new VueRouter({ /* ... */ })
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
在导航被确认之前,同时在组件内守卫和异步路由组件被解析之后调用:
router.beforeResolve((to, from, next) => {
console.log('导航即将完成')
next()
})
导航完成后触发,没有 next 函数:
router.afterEach((to, from) => {
document.title = to.meta.title || '默认标题'
})
在路由配置中定义:
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (isAdmin()) {
next()
} else {
next('/403')
}
}
}
]
在组件内部定义:
export default {
beforeRouteEnter(to, from, next) {
console.log('进入路由前')
next(vm => {
console.log('组件已创建')
})
},
beforeRouteUpdate(to, from, next) {
console.log('路由参数变化')
next()
},
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const answer = window.confirm('确定离开?')
if (answer) next()
else next(false)
} else {
next()
}
}
}
1. 触发导航
2. 调用失活组件的 beforeRouteLeave
3. 调用全局 beforeEach
4. 重用组件调用 beforeRouteUpdate
5. 路由配置调用 beforeEnter
6. 解析异步路由组件
7. 激活组件调用 beforeRouteEnter
8. 调用全局 beforeResolve
9. 导航确认
10. 调用全局 afterEach
11. 触发 DOM 更新
12. beforeRouteEnter 的 next 回调执行
next()
next(false)
next('/')
next({ name: 'home' })
next(error)
确保 next 只被调用一次
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
if (isAuthenticated()) {
next()
} else {
next('/login')
}
} else {
next()
}
})
import store from './store'
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const isLoggedIn = store.getters.isLoggedIn
if (requiresAuth && !isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else if (to.path === '/login' && isLoggedIn) {
next('/')
} else {
next()
}
})
router.beforeEach((to, from, next) => {
const title = to.meta.title
if (title) {
document.title = `${title} - 我的网站`
} else {
document.title = '我的网站'
}
next()
})
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
router.beforeEach((to, from, next) => {
NProgress.start()
next()
})
router.afterEach(() => {
NProgress.done()
})
router.beforeEach(async (to, from, next) => {
if (to.meta.preFetch) {
try {
await store.dispatch(to.meta.preFetch, to.params)
} catch (error) {
console.error('数据预取失败', error)
}
}
next()
})
router.beforeEach(async (to, from, next) => {
const requiredPermissions = to.meta.permissions || []
if (requiredPermissions.length === 0) {
next()
return
}
const userPermissions = store.getters.permissions
const hasPermission = requiredPermissions.some(
perm => userPermissions.includes(perm)
)
if (hasPermission) {
next()
} else {
next('/403')
}
})
let routesAdded = false
router.beforeEach(async (to, from, next) => {
if (routesAdded) {
next()
return
}
try {
const permissions = await store.dispatch('fetchPermissions')
const dynamicRoutes = generateRoutes(permissions)
router.addRoutes(dynamicRoutes)
routesAdded = true
next({ ...to, replace: true })
} catch (error) {
next('/login')
}
})
const cachePages = ['home', 'list']
router.beforeEach((to, from, next) => {
if (!cachePages.includes(to.name)) {
store.commit('CLEAR_CACHE')
}
next()
})
router.afterEach((to, from) => {
const log = {
from: from.path,
to: to.path,
timestamp: Date.now(),
userId: store.getters.userId
}
api.logVisit(log)
})
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(error => {
if (error.name === 'NavigationDuplicated') {
return this.currentRoute
}
return Promise.reject(error)
})
}
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(error => {
if (error.name === 'NavigationDuplicated') {
return this.currentRoute
}
return Promise.reject(error)
})
}
在渲染组件前调用,此时组件实例还未创建,无法访问 this:
export default {
beforeRouteEnter(to, from, next) {
console.log(this)
next(vm => {
console.log(vm)
vm.fetchData()
})
}
}
路由参数变化时调用,组件复用:
export default {
beforeRouteUpdate(to, from, next) {
if (to.params.id !== from.params.id) {
this.fetchUser(to.params.id)
}
next()
}
}
离开路由时调用,常用于防止用户未保存离开:
export default {
data() {
return {
form: {},
originalForm: {}
}
},
computed: {
hasChanges() {
return JSON.stringify(this.form) !== JSON.stringify(this.originalForm)
}
},
beforeRouteLeave(to, from, next) {
if (this.hasChanges) {
const answer = window.confirm('有未保存的更改,确定离开吗?')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
}
}
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from './store'
import NProgress from 'nprogress'
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'login',
component: () => import('./views/Login.vue'),
meta: { title: '登录' }
},
{
path: '/',
component: () => import('./views/Layout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'home',
component: () => import('./views/Home.vue'),
meta: { title: '首页' }
},
{
path: 'admin',
name: 'admin',
component: () => import('./views/Admin.vue'),
meta: {
title: '管理后台',
permissions: ['admin']
}
}
]
}
]
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
NProgress.start()
document.title = to.meta.title || '我的应用'
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const isLoggedIn = store.getters.isLoggedIn
if (requiresAuth && !isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else if (to.path === '/login' && isLoggedIn) {
next('/')
} else {
next()
}
})
router.beforeResolve((to, from, next) => {
const permissions = to.meta.permissions || []
if (permissions.length > 0) {
const userPermissions = store.getters.permissions
const hasPermission = permissions.some(p => userPermissions.includes(p))
if (!hasPermission) {
next('/403')
return
}
}
next()
})
router.afterEach(() => {
NProgress.done()
})
export default router
路由守卫要点: