前端面试系列【014】- vue-router 中如何保护指定路由的安全?

955 阅读5分钟

这个问题比较偏向于应用层面,而这方面的代码 vue-element-adminvue-antd-admin 都非常值得学习一番,强烈建议大家克隆下来细品。

对于这道题,回答思路应该从策略,落地到实现,如果能再深入到原理就再好不过了。

那么便开始回答吧~ (๑•̀ㅂ•́)و✧

通常在项目中,我们利用 vue-router 提供的路由守卫保护指定路由的安全。

那路由守卫又是什么呢?

路由守卫

我们先来看看官方的说明:

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

具体说来,这些路由守卫(官方叫做导航守卫)就是一系列的钩子函数,当 router 准备导航之前会批量的执行这些 hooks,而我们在这些 hooks 中就能做自己想做的事情。

路由守卫主要根据其作用范围分为全局、路由独享以及组件级的,下面分别介绍一下。

全局路由守卫

  • 全局前置守卫:
    • 注册:router.beforeEach
    • 触发:当导航触发的时候,所有注册的 beforeEach 将按顺序执行
    • 参数:
      • to:目标路由
      • from:当前路由
      • next:用于 resolve 触发的钩子。
        • next():resolve 当前钩子,进入下一个钩子,如果队列中所有钩子都执行完毕则将导航状态改为 confirmed
        • next(false):中断当前导航(即禁止跳转)
        • next(path) || next({path: '/path'}):跳转到 path 指定的路由
        • next(error):这个在平时工作中用到较少,如果给 next 传入一个 Error 实例,则终止导航,并且将该错误传递给 router.onError() 注册过的钩子
  • 全局解析守卫(版本 2.5.0+)
    • 注册:router.beforeResolve
    • 触发:在所有组件内守卫异步路由组件解析之后,解析守卫将被调用
    • 参数:同 router.beforeEach
  • 全局后置钩子
    • 注册:router.afterEach
    • 触发:确认导航之后
    • 参数:
      • to:目标路由
      • from:当前路由
    • tip:相信仔细的同学应该发现了,它的名字叫全局后置钩子,而不是全局后置守卫。因为和守卫不同,这类钩子并不接受 next 参数,无法改变导航本身,也就是说,用它们是没法保护路由安全的。

路由独享守卫

  • 注册

    routes: [
      {
        path: '/path',
        component: Component,
        beforeEnter: (to, from, next) => {}
      }
    ]
    
    
  • 触发:解析异步路由之前

  • 参数:同 router.beforeEach

组件内守卫

  • 路由前置守卫
    • 注册:直接在组件配置内注册 beforeRouteEnter 即可
    • 触发:解析异步路由之后,执行全局解析守卫之前
    • 参数:同 router.beforeEach
  • 路由更新守卫(版本 2.2.0+)
    • 注册:直接在组件配置内注册 beforeRouteUpdate 即可
    • 触发:调用全局前置守卫之后
    • 参数:同 router.beforeEach
    • tip:这个钩子中能访问到组件实例 this
  • 路由后置守卫
    • 注册:直接在组件配置内注册 beforeRouteLeave 即可
    • 触发:导航离开时调用
    • 参数:同 router.beforeEach
    • tip:
      • 可以访问组件实例 this
      • 实际上在执行流程中非常早就触发了

执行流程

整个执行流程稍微有些复杂,我们先将几个关键的步骤提出取来:

  • 导航触发
  • 解析异步路由
  • 导航确认
  • 更新 DOM

基于上面四个关键步骤,下面我们来看看整个执行流程

  • 在导航触发之后,我们要先离开当前路由,去往下一个路由,所以首先调用的是:beforeRouteLeave
  • 紧接着是一个新的开始,所以调用全局前置守卫:beforeEach
  • 重用的组件并没有销毁,但数据有可能更新,这个时候会调用它们的更新守卫:beforeRouteUpdate
  • 紧接着进入到路由配置,调用路由独享组件:beforeEnter
  • 这个时候会进行异步路由组件的解析
  • 解析完路由之后会进入到新的组件,也会调用其前置守卫:beforeRouteEnter
  • 这个时候,路由和组件都已经解析完毕,所以会调用全局解析守卫:beforeResolve
  • 至此,导航被确认,于是调用全局后置守卫 afterEach
  • 最后进入新的路由,触发 DOM 更新

应用

在平时的工作中,用到最多,用于保护指定路由安全的钩子是:beforeEach。在这个钩子中通过角色,token 等判断当前权限,然后在根据权限动态生成不同的路由,以及通过 next 方法来决定是否允许访问目标路由。

这里就以 vue-element-admin 的代码为例:

 router.beforeEach((to, from, next) => {
  /* 一些初始化操作 */
  const hasToken = getToken()
  const hasRoles = getRoles()
  // 如果有 token,并且有权限则允许访问
  if (hasToken && hasRoles) {
    next()
    // 如果有 token, 却没有权限, 则根据用户权限动态生成路由
  } else if (hasToken && !hasRoles) {
    createRoutes()
    // 没有 token,则检测目标路由是否在白名单内,比如 login、register、404 等
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

原理

关于 vue-router 的原理,如果读过我之前记录的手写简易 vue-router 应该会有比较清晰的思路,这里就大致阐述一下,不再深入,如果感兴趣的同学可以看看这篇。

首先路由要能够根据 URL 的变化而做出响应,那么就要能够监听 URL,vue-router 中最常用的有两种模式:

  • hash
  • history

abstract 用的非常少

以 hash 为例,我们可以通过 hashchange 监听 hash 的变化。

但仅仅监听 hash 是不够的的,因为路由需要做到的是,当地址发生变化的时候,动态的渲染出目标组件,所以我们用于监听 hash 的值一定要是响应式的,讲一个值变成响应式的在 vue 2.x 中通常有两种方式:

  • Vue.util.defineReactive
  • 利用 vue 实例的 data 将数据变成响应式

这就是 vue-router 的核心原理了:监听路由变化 + 响应式数据

下面可以看看本节的脑图,帮助梳理一下思路:

结语

更加阅读体验:014 - vue-router 中如何保护指定路由的安全?