导航守卫

所谓导航守卫,可以理解成拦截器或者路由发生变化时的钩子函数。vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。导航守卫可以分为多种,它们分别是:

  • 全局前置守卫。

  • 全局解析守卫。

  • 全局后置钩子。

  • 组件内守卫。

  • 路由配置守卫。

每当页面的路由变化时,可以把这种路由引起的路径变化称为 “导航”,这里的 “导航” 是一个动词,“守卫” 是一个名词,就是在这些 “导航” 有动作时来监听它们。

全局前置守卫

全局前置守卫需要直接注册在 router 对象上,可以使用 router.beforeEach 注册一个全局前置守卫,如示例代码7-4-1所示。

示例代码7-4-1 全局前置守卫的注册
// 配置路由信息
const router = VueRouter.createRouter({
    history: VueRouter.createWebHashHistory(),
    routes: [
        { path: '/user/:id', component: User },
    ]
})
router.beforeEach((to, from, next)=> {
    // 响应变化逻辑
    ...
    // next() // 如果使用了next参数,则必须调用next()方法
})

当一个路由发生改变时,全局前置守卫的回调方法便会执行,正因为是前置守卫,在改变之前便会进入这个方法,所以可以在这个方法中对路由相关的参数进行修改等,完成之后,必须调用 next() 方法才可以继续路由的工作。每个守卫方法接收 3 个参数:

  • to: Route 类型,表示即将进入的目标路由对象。

  • from: Route 类型,表示当前导航正要离开的路由对象。

  • next:可选,Function 类型,提供执行后续路由的参数,一定要调用该方法才能完成(resolve)整个钩子函数。执行效果取决于 next() 方法的调用参数:

    • next():进行管道中的下一个钩子。如果全部钩子执行完毕,则导航的状态就是确认的(confirmed)。

    • next(false):中断当前的导航。如果浏览器的 URL 改变了(可能是用户单击了浏览器的后退按钮),那么 URL 地址会重新设置到 from 路由对应的地址。

    • next('/') 或者 next({ path: '/' }):跳转到一个不同的地址。当前的导航被中断,然后执行一个新的导航。例如对之前的路由进行修改,然后将新的路由对象传递给 next() 方法。

    • next(error):如果传入 next 的参数是一个 Error 实例对象,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调方法(或回调函数)。

当选择使用了 next 参数时,请确保在任何情况下都要调用 next() 方法,否则守卫方法就不会被完成,而一直处于等待状态。

如果没有使用 next 参数,可以通过返回值的方式来完成或者终止守卫,使用方法和 next 类似,代码如下:

router.beforeEach((to, from)=> {
    return false // 相当于next(false)
    // 或者
    return { path: '...' }// 相当于next({ path: '...' })
})

全局解析守卫

router.beforeEach 之后还有一个守卫方法 router.beforeResolve,它用来注册一个全局守卫,称为全局解析守卫。用法与 router.beforeEach 类似,区别在于调用的时机,即全局解析守卫是在导航被确认之前,且在所有组件内守卫和异步路由组件被解析之后调用,如示例代码7-4-2所示。

router.beforeResolve((to, from, next)=> {
    // 响应变化逻辑
    ...
    next()
})

router.beforeResolve 是获取数据或执行任何其他操作(如果用户无法进入页面,用户希望避免执行的操作)的理想位置。

全局后置钩子

在了解了全局前置守卫和全局解析守卫之后,接下来学习一下全局后置钩子。这里解释一下为什么不叫 “守卫”,因为守卫一般可以对路由 router 对象进行修改和重定向,并且带有 next 参数,但是后置钩子不同,相当于只是提供了一个方法,让我们可以在路由切换之后执行相应的程序逻辑。这种钩子不会接受 next 函数,也不会改变导航本身,使用方法和全局前置守卫类似,如示例代码7-4-3所示。

router.afterEach((to, from)=> {
    ...
})

组件内的守卫

前面讲解的都是全局相关的守卫或者钩子,将这些方法设置在根组件上就可以很方便地获取对应的回调方法,并可在其中添加所需的处理逻辑。如果不需要在全局中设置,也可以单独给自己的组件设置一些导航守卫或者钩子,以达到监听路由变化的目的。

可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter。

  • beforeRouteUpdate。

  • beforeRouteLeave。

这些守卫的触发时机和使用方法如示例代码7-4-4所示。

const User = {
    template: '<div>用户id:{{$route.params.id}}</div>',
    beforeRouteEnter(to, from) {
        // 在渲染该组件的对应路由被验证前调用
        // 不能获取组件实例 'this'
        // 因为当守卫执行时,组件实例还没被创建
    },
    beforeRouteUpdate(to, from) {
        // 在当前路由改变,但是该组件被复用时调用
        // 举例来说,对于一个带有动态参数的路径 '/users/:id',在 '/users/1' 和'/users/2'
        // 之间跳转的时候,由于会渲染同样的 'UserDetails' 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用
        // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 'this'
    },
    beforeRouteLeave(to, from) {
        // 在导航离开渲染该组件的对应路由时调用
        // 与 'beforeRouteUpdate' 一样,它可以访问组件实例 'this'
    },
}

总结一下,beforeRouteEnterbeforeRouteLeave 这两个守卫很好理解,就是当导航进入该组件和离开该组件时调用,但是如果前后的导航是同一个组件,那么这种应用场合就属于组件复用,例如只改变参数,代码如下:

<router-link to="/user/1">导航user1</router-link>
<router-link to="/user/2">导航user2</router-link>
...
const User = {
    template: '<div>用户id:{{$route.params.id}}</div>',
}
...
const router = VueRouter.createRouter({
    history: VueRouter.createWebHashHistory(),
    routes: [
        { path: '/user/:id', component: User },
    ]
})

在这种应用场合下,beforeRouteEnterbeforeRouteLeave 这两个方法并不会触发,取而代之的是 beforeRouteUpdate 这个方法会在每次导航时触发。另外,在 beforeRouteEnter 方法中无法获取当前组件实例 this

因为 beforeRouteEnter 守卫在导航确认前被调用,守卫不能访问 this,所以即将登场的新组件还没创建。不过,可以通过传一个回调方法给 next() 来访问组件实例。在导航被确认时执行回调方法,并且把组件实例作为回调方法的参数。代码如下:

beforeRouteEnter(to, from, next) {
    next((vm)=>{
        // 通过'vm'访问组件实例
    })
}

与之前讲解的全局守卫一样,如果使用 next 参数,确保在任何情况下都要调用 next() 方法,否则守卫方法就会处于等待状态。beforeRouteLeave 离开守卫其中一个常见的应用场合是用来禁止用户在还未保存修改前突然离开,代码如下:

beforeRouteLeave(to, from , next)=> {
    var answer = window.confirm('尚未保存,是否离开?')
    if (answer) {
        next()
    } else {
        next(false)
    }
}

通过 next(false) 方法来取消用户离开该组件,进入其他导航。

路由配置守卫

除了一些全局守卫和组件内部的守卫外,也可以在路由配置上直接定义守卫,例如 beforeEnter 守卫,如示例代码7-4-5所示。

示例代码7-4-5 路由配置守卫的定义
const routes = [
    {
        path: '/users/:id',
        component: User,
        beforeEnter: (to, from) => {
            // reject the navigation
            return false
        },
    },
]

beforeEnter 守卫的触发时机与 beforeRouteEnter 方法类似,但是它要早于 beforeRouteEnter 的触发,同样要记得如果使用了 next 参数,则需要调用 next() 方法。当需要单独给一个路由配置时,可以采用这种方法。

下面总结一下所有守卫和钩子函数的整个触发流程:

  1. 导航被触发。

  2. 在失活的组件中调用 beforeRouteLeave 离开守卫。

  3. 调用全局的 beforeEach 守卫。

  4. 在复用的组件中调用 beforeRouteUpdate 守卫。

  5. 在路由配置中调用 beforeEnter

  6. 解析异步路由组件。

  7. 在被激活的组件中调用 beforeRouteEnter

  8. 调用全局的 beforeResolve 守卫。

  9. 导航被确认。

  10. 调用全局的 afterEach 钩子。

  11. 触发 DOM 更新。

  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

Vue Router 的导航守卫提供了丰富的接口,可以在页面切换时添加项目的业务逻辑,对于开发大型单页面应用很有帮助。例如在渲染用户信息时,需要从服务器获取用户的数据,即可以在 User 组件的 beforeRouteEnter 方法中获取数据,如示例代码7-4-6所示。

示例代码7-4-6 在beforeRouteEnter方法中获取数据
const User = {
    template: '<div>用户id:{{$route.params.id}}</div>',
    beforeRouteEnter (to, from, next) {
        next((vm)=>{
            // 通过'vm'访问组件实例
            vm.getUserData()
        })
    },
    methods:{
        getUserData(){
            ... //ajax请求逻辑
        }
    }
}