element label动态赋值_浅析 vuerouter 源码和动态路由权限分配

91e2ab185c239da18678797f98c198f8.png

背景

上月立过一个 flag,看完 vue-router 的源码,可到后面逐渐发现 vue-router 的源码并不是像很多总结的文章那么容易理解,阅读过你就会发现里面的很多地方都会有多层的函数调用关系,还有大量的 this 指向问题,而且会有很多辅助函数需要去理解。但还是坚持啃下来了(当然还没看完,内容是真的多),下面是我在政采云(实习)工作闲暇时间阅读源码的一些感悟和总结,并带分析了大三时期使用的 vue-element-admin (https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/#%E5%8A%9F%E8%83%BD) 这个 vuer 无所不知的后台框架的动态路由权限控制原理。顺便附带本文实践 demo 地址: 基于后台框架开发的 学生管理系统 (https://github.com/251205668/student-admin-template)。

vue-router 源码分析

b40b93fd5c5e6260f249abe6b236c500.png

首先阅读源码之前最好是将 Vuevue-router 的源码克隆下来,然后第一遍阅读建议先跟着 官方文档 (https://router.vuejs.org/zh/) 先走一遍基础用法,然后第二遍开始阅读源码,先理清楚各层级目录的作用和抽出一些核心的文件出来,过一遍代码的同时写个小的 demo 边看边打断点调试,看不懂没关系,可以边看边参考一些总结的比较好的文章,最后将比较重要的原理过程根据自己的理解整理出来,然后画一画相关的知识脑图加深印象。

前置知识: flow 语法

JS 在编译过程中可能看不出一些隐蔽的错误,但在运行过程中会报各种各样的 bug。flow (https://flow.org/en/docs/getting-started/) 的作用就是编译期间进行静态类型检查,尽早发现错误,抛出异常。

VueVue-router 等大型项目往往需要这种工具去做静态类型检查以保证代码的可维护性和可靠性。本文所分析的 vue-router 源码中就大量的采用了 flow 去编写函数,所以学习 flow 的语法是有必要的。

首先安装 flow 环境,初始化环境

npm install flow-bin -g
flow init

index.js 中输入这一段报错的代码

/*@flow*/
function add(x: string, y: number): number {
return x + y
}
add(2, 11)

在控制台输入 flow,这个时候不出意外就会抛出异常提示,这就是简单的 flow 使用方法。

具体用法还需要参考 flow官网 (https://flow.org/en/docs/types/primitives/),另外这种语法是类似于 TypeScript (https://www.typescriptlang.org/) 的。

注册

43d0f8397a3d95cb3828ea423ccff38e.png

我们平时在使用 vue-router 的时候通常需要在 main.js 中初始化 Vue 实例时将 vue-router 实例对象当做参数传入

例如:

import Router from 'vue-router'
Vue.use(Router)
const routes = [
   {
     path: '/student',
    name: 'student',
    component: Layout,
    meta: { title: '学生信息查询', icon: 'documentation', roles: ['student'] },
    children: [
      {
        path: 'info',
        component: () => import('@/views/student/info'),
        name: 'studentInfo',
        meta: { title: '信息查询', icon: 'form' }
      },
      {
        path: 'score',
        component: () => import('@/views/student/score'),
        name: 'studentScore',
        meta: { title: '成绩查询', icon: 'score' }
      }
    ]
  }
  ...
];
const router = new Router({
  mode: "history",
  linkActiveClass: "active",
  base: process.env.BASE_URL,
  routes
});
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount("#app");

Vue.use

那么 Vue.use(Router) 又在做什么事情呢

问题定位到 Vue 源码中的 src/core/global-api/use.js 源码地址 (https://github.com/vuejs/vue/blob/dev/src/core/global-api/use.js)

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 拿到 installPlugins 
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 保证不会重复注册
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // 获取第一个参数 plugins 以外的参数
    const args = toArray(arguments, 1)
    // 将 Vue 实例添加到参数
    args.unshift(this)
    // 执行 plugin 的 install 方法 每个 insatll 方法的第一个参数都会变成 Vue,不需要额外引入
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 最后用 installPlugins 保存 
    installedPlugins.push(plugin)
    return this
  }
}

可以看到 Vueuse 方法会接受一个 plugin 参数,然后使用 installPlugins 数组 保存已经注册过的 plugin。首先保证 plugin 不被重复注册,然后将 Vue 从函数参数中取出,将整个 Vue 作为 plugininstall 方法的第一个参数,这样做的好处就是不需要麻烦的另外引入 Vue,便于操作。接着就去判断 plugin 上是否存在 install 方法。存在则将赋值后的参数传入执行 ,最后将所有的存在 install 方法的 plugin 交给 installPlugins维护。

install

了解清楚 Vue.use 的结构之后,可以得出 Vue 注册插件其实就是在执行插件的 install 方法,参数的第一项就是 Vue,所以我们将代码定位到 vue-router 源码中的 src/install.js 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/install.js)

// 保存 Vue 的局部变量
export let _Vue
export function install (Vue) {
  // 如果已安装
  if (install.installed && _Vue === Vue) return
  install.installed = true
 // 局部变量保留传入的 Vue
  _Vue = Vue
  const isDef = v => v !== undefined
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 全局混入钩子函数 每个组件都会有这些钩子函数,执行就会走这里的逻辑
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        // new Vue 时传入的根组件 router router对象传入时就可以拿到 this.$options.router
        // 根 router
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // 变成响应式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 非根组件访问根组件通过$parent
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  // 原型加入 $router 和 $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
// 全局注册
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
// 获取合并策略
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

可以看到这段代码核心部分就是在执行 install 方法时使用 mixin 的方式将每个组件都混入 beforeCreate,destroyed 这两个生命周期钩子。在 beforeCreate 函数中会去判断当前传入的 router 实例是否是根组件,如果是,则将 _routerRoot 赋值为当前组件实例、_router 赋值为传入的VueRouter 实例对象,接着执行 init 方法初始化 router,然后将 this_route 响应式化。非根组件的话 _routerRoot 指向 $parent 父实例。然后执行 registerInstance(this,this) 方法,该方法后会,接着原型加入 $router$route,最后注册 RouterViewRouterLink,这就是整个 install 的过程。

小结

Vue.use(plugin) 实际上在执行 plugin上的 install 方法,insatll 方法有个重要的步骤:

  • 使用 mixin 在组件中混入 beforeCreate , destory 这俩个生命周期钩子
  • beforeCreate 这个钩子进行初始化。
  • 全局注册 router-viewrouter-link组件

VueRouter

接着就是这个最重要的 class : VueRouter。这一部分代码比较多,所以不一一列举,挑重点分析。vueRouter源码地址 (https://github.com/vuejs/vue-router/blob/v3.1.2/src/index.js)。

构造函数

  constructor (options: RouterOptions = {}) {
    this.app  = null
    this.apps = []
    // 传入的配置项
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)
    // 一般分两种模式 hash 和 history 路由 第三种是抽象模式
    let mode = options.mode || 'hash'
    // 判断当前传入的配置是否能使用 history 模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    // 降级处理
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 根据模式实例化不同的 history,history 对象会对路由进行管理 继承于history class
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

首先在初始化 vueRouter 整个对象时定义了许多变量,app 代表 Vue 实例,options 代表传入的配置参数,然后就是路由拦截有用的 hooks 和重要的 matcher (后文会写到)。构造函数其实在做两件事情: 1. 确定当前路由使用的 mode;2. 实例化对应的 history 对象。

init

接着完成实例化 vueRouter 之后,如果这个实例传入后,也就是刚开始说的将 vueRouter 实例在初始化 Vue 时传入,它会在执行 beforeCreate 时执行 init 方法

init (app: any) {
  ...
  this.apps.push(app)
  // 确保后面的逻辑只走一次
  if (this.app) {
    return
  }
  // 保存 Vue 实例
  this.app = app
  const history = this.history
  // 拿到 history 实例之后,调用 transitionTo 进行路由过渡
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }
}

init 方法传入 Vue 实例,保存到 this.apps 当中。Vue实例 会取出当前的 this.history,如果是哈希路由,先走 setupHashListener 函数,然后调一个关键的函数 transitionTo 路由过渡,这个函数其实调用了 this.matcher.match 去匹配。

小结

首先在 vueRouter 构造函数执行完会完成路由模式的选择,生成 matcher ,然后初始化路由需要传入 vueRouter 实例对象,在组件初始化阶段执行 beforeCreate 钩子,调用 init 方法,接着拿到 this.history 去调用 transitionTo 进行路由过渡。

Matcher

8c954dc772396df2578986d6ad619b1a.png

之前在 vueRouter 的构造函数中初始化了 macther,本节将详细分析下面这句代码到底在做什么事情,以及 match 方法在做什么 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/create-matcher.js)。

 this.matcher = createMatcher(options.routes || [], this)

首先将代码定位到create-matcher.js

export function createMatcher (
  routes: Array,
  router: VueRouter): Matcher {
  // 创建映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // 添加动态路由
  function addRoutes(routes){...}
  // 计算新路径
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location): Route {...}
  // ... 后面的一些方法暂不展开
  
   return {
    match,
    addRoutes
  }
}

createMatcher 接受俩参数,分别是 routes,这个就是我们平时在 router.js 定义的路由表配置,然后还有一个参数是 router 他是 new vueRouter 返回的实例。

createRouteMap

下面这句代码是在创建一张 path-record,name-record 的映射表,我们将代码定位到 create-route-map.js 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/create-route-map.js)

export function createRouteMap (
  routes: Array,
  oldPathList?: Array,
  oldPathMap?: Dictionary,
  oldNameMap?: Dictionary): {
  pathList: Array,pathMap: Dictionary,nameMap: Dictionary
} {// 记录所有的 pathconst pathList: Array = oldPathList || []// 记录 path-RouteRecord 的 Mapconst pathMap: Dictionary = oldPathMap || Object.create(null)// 记录 name-RouteRecord 的 Mapconst nameMap: Dictionary = oldNameMap || Object.create(null)// 遍历所有的 route 生成对应映射表
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })// 调整优先级for (let i = 0, l = pathList.length; i     if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }return {
    pathList,
    pathMap,
    nameMap
  }
}

createRouteMap 需要传入路由配置,支持传入旧路径数组和旧的 Map 这一步是为后面递归和 addRoutes 做好准备。首先用三个变量记录 path,pathMap,nameMap,接着我们来看 addRouteRecord 这个核心方法。这一块代码太多了,列举几个重要的步骤

// 解析路径
const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
// 拼接路径
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 记录路由信息的关键对象,后续会依此建立映射表
const record: RouteRecord = {
  path: normalizedPath,
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  // route 对应的组件
  components: route.components || { default: route.component },
  // 组件实例
  instances: {},
  name,
  parent,
  matchAs,
  redirect: route.redirect,
  beforeEnter: route.beforeEnter,
  meta: route.meta || {},
  props: route.props == null
    ? {}
    : route.components
      ? route.props
      : { default: route.props }
}

使用 recod 对象 记录路由配置有利于后续路径切换时计算出新路径,这里的 path 其实是通过传入父级 record 对象的path和当前 path 拼接出来的  。然后 regex 使用一个库将 path 解析为正则表达式。如果 route 有子节点就递归调用 addRouteRecord

 // 如果有 children 递归调用 addRouteRecord
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })

最后映射两张表,并将 record·path 保存进 pathList,nameMap 逻辑相似就不列举了

  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

废了这么大劲将 pathListpathMapnameMap 抽出来是为啥呢? 首先 pathList 是记录路由配置所有的 path,然后 pathMapnameMap 方便我们传入 path 或者 name 快速定位到一个 record,然后辅助后续路径切换计算路由的。

addRoutes

这是在 vue2.2.0 之后新添加的 api ,或许很多情况路由并不是写死的,需要动态添加路由。有了前面的 createRouteMap 的基础上我们只需要传入 routes 即可,他就能在原基础上修改

function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}

并且看到在 createMathcer 最后返回了这个方法,所以我们就可以使用这个方法

return {
    match,
    addRoutes
  }

match

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location): Route {
  ...
}

接下来就是 match 方法,它接收 3 个参数,其中 rawRawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象;currentRouteRoute 类型,它表示当前的路径;redirectedFrom 和重定向相关。match 方法返回的是一个路径,它的作用是根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径并返回。至于他是如何计算出这条路径的,可以详细看一下如何计算出locationnormalizeLocation 方法和 _createRoute 方法。

小结

  • createMatcher: 根据路由的配置描述建立映射表,包括路径、名称到路由 record 的映射关系, 最重要的就是 createRouteMap 这个方法,这里也是动态路由匹配和嵌套路由的原理。
  • addRoutes: 动态添加路由配置
  • match: 根据传入的 raw 和当前的路径 currentRoute 计算出一个新的路径并返回。

路由模式

9f8e922a8b5dfc99505b4f26af347cfe.png

vue-router 支持三种路由模式(mode):hashhistoryabstract,其中 abstract 是在非浏览器环境下使用的路由模式 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/index.js)。

这一部分在前面初始化 vueRouter 对象时提到过,首先拿到配置项的模式,然后根据当前传入的配置判断当前浏览器是否支持这种模式,默认 IE9 以下会降级为 hash。然后根据不同的模式去初始化不同的 history 实例。

    // 一般分两种模式 hash 和 history 路由 第三种是抽象模式不常用
    let mode = options.mode || 'hash'
    // 判断当前传入的配置是否能使用 history 模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    // 降级处理
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 根据模式实例化不同的 history history 对象会对路由进行管理 继承于 history class
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }

小结

vue-router 支持三种路由模式,hashhistory和 abstract。默认为 hash,如果当前浏览器不支持 history则会做降级处理,然后完成 history 的初始化。

路由切换

6cb0a1c8f176eacd3143ece8bcb6225a.png切换 url 主要是调用了 push 方法,下面以哈希模式为例,分析push方法实现的原理 。push 方法切换路由的实现原理 源码地址 (https://github.com/vuejs/vue-router/blob/dev/src/history/hash.js)

首先在 src/index.js 下找到 vueRouter 定义的 push 方法

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

接着我们需要定位到 history/hash.js。这里首先获取到当前路径然后调用了 transitionTo 做路径切换,在回调函数当中执行 pushHash 这个核心方法。

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    // 路径切换的回调函数中调用 pushHash
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

pushHash 方法在做完浏览器兼容判断后调用的 pushState 方法,将 url 传入

export function pushState (url?: string, replace?: boolean) {
  const history = window.history
  try {
   // 调用浏览器原生的 history 的 pushState 接口或者 replaceState 接口,pushState 方法会将 url 入栈
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

可以发现,push 底层调用了浏览器原生的 historypushStatereplaceState 方法,不是 replace 模式 会将 url 推历史栈当中。

另外提一嘴拼接哈希的原理

源码位置 (https://github.com/vuejs/vue-router/blob/dev/src/history/hash.js)

初始化 HashHistory 时,构造函数会执行 ensureSlash 这个方法

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    ...
    ensureSlash()
  }
  ...
  }

这个方法首先调用 getHash,然后执行 replaceHash()

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

下面是这几个方法

export function getHash (): string {
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : href.slice(index + 1)
}
// 真正拼接哈希的方法 
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
export function replaceState (url?: string) {
  pushState(url, true)
}

举个例子来说: 假设当前URL是 http://localhost:8080,path 为空,执行 replcaeHash('/' + path),然后内部执行 getUrl 计算出 urlhttp://localhost:8080/#/,最后执行 pushState(url,true),就大功告成了!

小结

hash 模式的 push 方法会调用路径切换方法 transitionTo,接着在回调函数中调用pushHash方法,这个方法调用的 pushState 方法底层是调用了浏览器原生 history 的方法。pushreplace 的区别就在于一个将 url 推入了历史栈,一个没有,最直观的体现就是 replace 模式下浏览器点击后退不会回到上一个路由去 ,另一个则可以。

router-view & router-link

522ac314eca80e4239f37bb00b6dbf65.png

vue-routerinstall 时全局注册了两个组件一个是 router-view 一个是 router-link,这两个组件都是典型的函数式组件。源码地址 (https://github.com/vuejs/vue-router/tree/dev/src/components)

router-view

首先在 router 组件执行 beforeCreate 这个钩子时,把 this._route 转为了响应式的一个对象

 Vue.util.defineReactive(this, '_route', this._router.history.current)

所以说每次路由切换都会触发 router-view 重新 render 从而渲染出新的视图。

核心的 render 函数作用请看代码注释

  render (_, { props, children, parent, data }) {
    ...
    // 通过 depth 由 router-view 组件向上遍历直到根组件,遇到其他的 router-view 组件则路由深度+1 这里的 depth 最直接的作用就是帮助找到对应的 record
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      // parent.$vnode.data.routerView 为 true 则代表向上寻找的组件也存在嵌套的 router-view 
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    if (inactive) {
      return h(cache[name], data, children)
    }
   // 通过 matched 记录寻找出对应的 RouteRecord 
    const matched = route.matched[depth]
    if (!matched) {
      cache[name] = null
      return h()
    }
 // 通过 RouteRecord 找到 component
    const component = cache[name] = matched.components[name]
   // 往父组件注册 registerRouteInstance 方法
    data.registerRouteInstance = (vm, val) => {     
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
  // 渲染组件
    return h(component, data, children)
  }

触发更新也就是 setter 的调用,位于 src/index.js,当修改 _route 就会触发更新。

history.listen(route => {
  this.apps.forEach((app) => {
    // 触发 setter
    app._route = route
  })
})

router-link

分析几个重要的部分:

  • 设置 active 路由样式

router-link 之所以可以添加 router-link-activerouter-link-exact-active 这两个 class 去修改样式,是因为在执行 render 函数时,会根据当前的路由状态,给渲染出来的 active 元素添加 class

render (h: Function) {
  ...
  const globalActiveClass = router.options.linkActiveClass
  const globalExactActiveClass = router.options.linkExactActiveClass
  // Support global empty active class
  const activeClassFallback = globalActiveClass == null
    ? 'router-link-active'
    : globalActiveClass
  const exactActiveClassFallback = globalExactActiveClass == null
    ? 'router-link-exact-active'
    : globalExactActiveClass
    ...
}
  • router-link 默认渲染为 a 标签,如果不是会去向上查找出第一个 a 标签
 if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first  child and apply listener and href
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the  is a static node
        a.isStatic = false
        const aData = (a.data = extend({}, a.data))
        aData.on = on
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
      } else {
        // 不存在则渲染本身元素
        data.on = on
      }
    }
  • 切换路由,触发相应事件
const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      // replace路由
      router.replace(location)
    } else {
      // push 路由
      router.push(location)
    }
  }
}

权限控制动态路由原理分析

我相信,开发过后台项目的同学经常会碰到以下的场景: 一个系统分为不同的角色,然后不同的角色对应不同的操作菜单和操作权限。例如: 教师可以查询教师自己的个人信息查询然后还可以查询操作学生的信息和学生的成绩系统、学生用户只允许查询个人成绩和信息,不允许更改。在 vue2.2.0 之前还没有加入 addRoutes 这个 API 是十分困难的的。

目前主流的路由权限控制的方式是:

  1. 登录时获取 token 保存到本地,接着前端会携带 token 再调用获取用户信息的接口获取当前用户的角色信息。
  2. 前端再根据当前的角色计算出相应的路由表拼接到常规路由表后面。

登录生成动态路由全过程

了解 如何控制动态路由之后,下面是一张全过程流程图

16289bdcd996f94d93f7e6095dadd0ff.png

前端在 beforeEach 中判断:

  • 缓存中存在 JWT 令牌
    • 访问/login: 重定向到首页 /
    • 访问/login以外的路由:  首次访问,获取用户角色信息,然后生成动态路由,然后访问以 replace 模式访问 /xxx 路由。这种模式用户在登录之后不会在 history 存放记录
  • 不存在 JWT 令牌
    • 路由在白名单中: 正常访问 /xxx 路由
    • 不在白名单中: 重定向到 /login 页面

结合框架源码分析

下面结合 vue-element-admin 的源码分析该框架中如何处理路由逻辑的。

路由访问逻辑分析

首先可以定位到和入口文件 main.js 同级的 permission.js, 全局路由守卫处理就在此。源码地址 (https://github.com/251205668/student-admin-template/blob/master/src/permission.js)

const whiteList = ['/login', '/register'] // 路由白名单,不会重定向
// 全局路由守卫
router.beforeEach(async(to, from, next) => {
  NProgress.start() //路由加载进度条
  // 设置 meta 标题
  document.title = getPageTitle(to.meta.title)
  // 判断 token 是否存在
  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      // 有 token 跳转首页
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // 获取动态路由,添加到路由表中
          const { roles } = await store.dispatch('user/getInfo')
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          router.addRoutes(accessRoutes)
          //  使用 replace 访问路由,不会在 history 中留下记录,登录到 dashbord 时回退空白页面
          next({ ...to, replace: true })
        } catch (error) {
          next('/login')
          NProgress.done()
        }
      }
    }
  } else {
    // 无 token
    // 白名单不用重定向 直接访问
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      // 携带参数为重定向到前往的路径
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

这里的代码我都添加了注释方便大家好去理解,总结为一句话就是访问路由 /xxx,首先需要校验 token 是否存在,如果有就判断是否访问的是登录路由,走的不是登录路由则需要判断该用户是否是第一访问首页,然后生成动态路由,如果走的是登录路由则直接定位到首页,如果没有 token 就去检查路由是否在白名单(任何情况都能访问的路由),在的话就访问,否则重定向回登录页面。

下面是经过全局守卫后路由变化的截图

92c53e43ea59f9a849ce0d28b87c440c.png

结合Vuex生成动态路由

下面就是分析这一步 const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 是怎么把路由生成出来的。源码地址 (https://github.com/251205668/student-admin-template/blob/master/src/store/modules/permission.js)

首先 vue-element-admin 中路由是分为两种的:

  • constantRoutes: 不需要权限判断的路由
  • asyncRoutes: 需要动态判断权限的路由
// 无需校验身份路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  }
  ...
  ],
 // 需要校验身份路由 
export const asyncRoutes = [
  // 学生角色路由
  {
    path: '/student',
    name: 'student',
    component: Layout,
    meta: { title: '学生信息查询', icon: 'documentation', roles: ['student'] },
    children: [
      {
        path: 'info',
        component: () => import('@/views/student/info'),
        name: 'studentInfo',
        meta: { title: '信息查询', icon: 'form' }
      },
      {
        path: 'score',
        component: () => import('@/views/student/score'),
        name: 'studentScore',
        meta: { title: '成绩查询', icon: 'score' }
      }
    ]
  }]
  ...

生成动态路由的源码位于 src/store/modules/permission.js 中的 generateRoutes 方法,源码如下:

 generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
      // 不是 admin 去遍历生成对应的权限路由表
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      // vuex 中保存异步路由和常规路由
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }

route.js 读取 asyncRoutesconstantRoutes 之后首先判断当前角色是否是 admin,是的话默认超级管理员能够访问所有的路由,当然这里也可以自定义,否则去过滤出路由权限路由表,然后保存到 Vuex 中。最后将过滤之后的 asyncRoutesconstantRoutes 进行合并。过滤权限路由的源码如下:

export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route => {
    // 浅拷贝
    const tmp = { ...route }
    // 过滤出权限路由
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}

首先定义一个空数组,对传入 asyncRoutes 进行遍历,判断每个路由是否具有权限,未命中的权限路由直接舍弃 判断权限方法如下:

function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    // roles 有对应路由元定义的 role 就返回 true
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

接着需要判断二级路由、三级路由等等的情况,再做一层迭代处理,最后将过滤出来的路由推进数组返回。然后追加到 constantRoutes 后面

 SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }

动态路由生成全过程

871239c41c0e8e373b82b302030f801d.png

总结

  • vue-router 源码分析部分

    • 注册: 执行 install 方法,注入生命周期钩子初始化
    • vueRouter: 当组件执行 beforeCreate 传入 router 实例时,执行 init 函数,然后执行 history.transitionTo 路由过渡
    • matcher : 根据传入的 routes 配置创建对应的 pathMapnameMap ,可以根据传入的位置和路径计算出新的位置并匹配对应的 record
    • 路由模式: 路由模式在初始化 vueRouter 时完成匹配,如果浏览器不支持则会降级
    • 路由 切换: 哈希模式下底层使用了浏览器原生的 pushStatereplaceState 方法
    • router-view: 调用父组件上存储的 $route.match 控制路由对应的组件的渲染情况,并且支持嵌套。
    • router-link: 通过 to 来决定点击事件跳转的目标路由组件,并且支持渲染成不同的 tag,还可以修改激活路由的样式。
  • 权限控制动态路由部分

    • 路由逻辑: 全局路由拦截,从缓存中获取令牌,存在的话如果首次进入路由需要获取用户信息,生成动态路由,这里需要处理 /login 特殊情况,不存在则判断白名单然后走对应的逻辑
    • 动态生成路由: 传入需要 router.js 定义的两种路由。判断当前身份是否是管理员,是则直接拼接,否则需要过滤出具备权限的路由,最后拼接到常规路由后面,通过 addRoutes 追加。

读后感想

或许阅读源码的作用不能像一篇开发文档一样直接立马对日常开发有所帮助,但是它的影响是长远的,在读源码的过程中都可以学到众多知识,类似闭包、设计模式、时间循环、回调等等 JS 进阶技能,并稳固并提升了你的 JS 基础。当然这篇文章是有缺陷的,有几个地方都没有分析到,比如导航守卫实现原理和路由懒加载实现原理,这一部分,我还在摸索当中。

如果一味的死记硬背一些所谓的面经,或者直接死记硬背相关的框架行为或者 API ,你很难在遇到比较复杂的问题下面去快速定位问题,了解怎么去解决问题,而且我发现很多人在使用一个新框架之后遇到点问题都会立马去提对应的 Issues,以至于很多流行框架 Issues 超过几百个或者几千个,但是许多问题都是因为我们并未按照设计者开发初设定的方向才导致错误的,更多都是些粗心大意造成的问题。

参考文章

带你全面分析 vue-router 源码 (万字长文) (https://juejin.im/post/6844904064367460366)

vuejs 源码解析 (https://github.com/answershuto/learnVue)

近期1024程序员们过节,他们都在干这件事....面试官:聊聊对Vue.js框架的理解4f4a8209a6b015b9b936bfe4a9fd39aa.png若此文有用,何不素质三连❤️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

世界领先的界面设计公司:The Skins Factory

该公司的网站: http://www.theskinsfactory.com/skinsfactory/ 该公司诞生于2000年,由一群狂热的界面爱好者,带着对GUI的热情和大胆的洞察力创立。很快,皮肤工厂便成长为世界领先的、真正的、革命性界面解决方案提供商。 更多的精…

HDU 1253 胜利大逃亡 题解

胜利大逃亡 Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 44540 Accepted Submission(s): 15483 Problem DescriptionIgnatius被魔王抓走了,有一天魔王出差去了,这可是Ignatius逃亡的好机会.魔王住在一个城堡…

lstm需要优化的参数_使用PyTorch手写代码从头构建LSTM,更深入的理解其工作原理...

这是一个造轮子的过程,但是从头构建LSTM能够使我们对体系结构进行更加了解,并将我们的研究带入下一个层次。LSTM单元是递归神经网络深度学习研究领域中最有趣的结构之一:它不仅使模型能够从长序列中学习,而且还为长、短期记忆创建…

有哪些漂亮的中国风 LOGO 设计?

提到中国风的logo,我觉得首先登场的应该是北京故宫博物院的logo,铛!故宫博物院的logo,从颜色,到外形,到元素,无一例外,充满了中国风的味道,可谓是中国风中的典型。同一风…

python3常用模块_Python3 常用模块

一、time与datetime模块 在Python中,通常有这几种方式来表示时间: 时间戳(timestamp):通常来说,时间戳表示的是从1970年1月1日00:00:00开始按秒计算的偏移量。我们运行“type(time.time())”,返回的是float类型。 格式…

地方政府不愿房价下跌 救市或化解房地产调控

地方政府不愿房价下跌 "救市"或化解房地产调控 2008年05月09日 07:29:38  来源:上海证券报 漫画 刘道伟 由于房地产业与地方政府利益攸关,地方政府最不愿意看到房价下跌。中央房地产调控政策刚刚导致部分城市的房价步入调整,一些…

App移动端性能工具调研

使用GT的差异化场景平台描述release版本development版本Android在Android平台上,如果希望使用GT的高级功能,如“插桩”等,就必须将GT的SDK嵌入到被调测的应用的工程里,再配合安装好的GT使用。支持AndroidiOS在iOS平台上&#xff0…

UITabBar Contoller

。UITabBar中的UIViewController获得控制权:在TabBar文件中添加:IBOutlet UITabBar *myTabBar; //在xib中连接tabBar;(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:      (UIViewControlle…

python3.5安装pip_win10上python3.5.2第三方库安装(运用pip)

1 首先在python官网下载并安装python。我这儿用的是python3.5.2,其自带了pip。如果你选择的版本没有自带pip,那么请查找其他的安装教程。 2 python安装好以后,我在其自带的命令提示符窗口中输入了pip,结果尴尬了,提示我…

语法上的小trick

语法上的小trick 构造函数 虽然不写构造函数也是可以的,但是可能会开翻车,所以还是写上吧。: 提供三种写法: ​ 使用的时候只用: 注意,这里的A[i]gg(3,3,3)的“gg”不能打括号,否则就是强制转换…

Ubuntu18.04如何让桌面软件默认root权限运行?

什么是gksu? 什么是gksu:Linxu中的gksu是系统中的su/sudo工具,如果安装了gksu,在终端中键入gksu会弹出一个对话框. 安装gksu: 在Ubuntu之前的版本中是继承gksu工具的,但是在Ubutu18.04中并没有集成, 在Elementary OS中连gksu的APT源都没有. Ubuntu18.04 安装和使用gksu: seven…

win10诊断启动后联网_小技巧:win10网络共享文件夹出现错误无法访问如何解决?...

win10系统共享文件夹时在资源管理器中的网络里能够看到所共享的文件夹,但在打开文件夹时却出现 Windows无法访问 Desktop-r8ceh55新建文件夹 请检查名称的拼写。否则,网络可能有问题。要尝试识别并解决网络问题,请单击“诊断”的错误提示&…

两段关于统计日期的sql语句

统计月份:selectleft(convert(char(10),[Article_TimeDate],102),7) as月份, count(*) as数量from[hdsource].[dbo].[article]groupbyleft(convert(char(10),[Article_TimeDate],102),7)orderby1统计年份: selectleft(convert(char(10),[Article_TimeDat…

sklearn 安装_sklearn-classification_report

原型sklearn.metrics.classification_report(y_true, y_pred, labelsNone, target_namesNone, sample_weightNone, digits2)参数y_true:1维数组或标签指示数组/离散矩阵,样本实际类别值列表y_pred:1维数组或标签指示数组/离散矩阵&#xff0c…

Python标准库之csv(1)

1.Python处理csv文件之csv.writer() import csvdef csv_write(path,data):with open(path,w,encodingutf-8,newline) as f:writer csv.writer(f,dialectexcel)for row in data:writer.writerow(row)return True 调用上面的函数 data [[Name,Height],[Keys,176cm],[HongPing,1…

我用代码来给你们分析一个赚钱的技巧

2019独角兽企业重金招聘Python工程师标准>>> 赚钱是个俗气的话题,但又是人人都绕不开的事情。我今天来“科学”地触碰下这个话题。 谈赚钱,就会谈到理财、投资,谈到炒股。有这样一个笑话: 问:如何成为百万富…

idea中自动deployment的步骤

转载于:https://www.cnblogs.com/littlehb/p/11322666.html

python怎么编辑文件_如何使用python中的方法对文件进行修改文件名

在使用python语言中的方法操作文件时,打开方法可以直接使用open,但是对文件重命名需要调用os模块中的方法,删除文件也是工具/原料 python 编辑器 截图工具 台式机 方法/步骤 1 进入到python安装文件目录,新建txt文件kou.txt2 打开…

球迷必备Euro Cup Mobile 2008 !-dopod touch diamond试用之欧洲杯

欧洲杯从6月8日开始,到现在已经进行了半个多月了。到今天为止已经进入到了尾声,也到了激战正酣的时刻!(相信在国足出线无望后大伙的目光都聚集到了欧洲杯上) 但是平时上班忙,晚上也没法熬夜看球,哥们心理着急呀。白天上…

python中意外缩进是什么意思_如何处理python中的“意外缩进”?

慕工程0101907 Python在行的开头使用间距来确定代码块何时开始和结束。你可以得到的错误是:意外的缩进。这行代码在开始时比前一行有更多空格,但前一行不是子块的开头(例如if / while / for语句)。块中的所有代码行必须以完全相同…