乐趣区

关于vue-router:浅析-vuerouter-源码和动态路由权限分配

72 篇原创好文~ 本文首发于政采云前端团队博客:浅析 vue-router 源码和动静路由权限调配

浅析 vue-router 源码和动静路由权限调配

背景

上月立过一个 flag,看完 vue-router 的源码,可到前面逐步发现 vue-router 的源码并不是像很多总结的文章那么容易了解,浏览过你就会发现外面的很多中央都会有多层的函数调用关系,还有大量的 this 指向问题,而且会有很多辅助函数须要去了解。但还是保持啃下来了 (当然还没看完,内容是真的多),上面是我在政采云(实习) 工作空闲工夫浏览源码的一些感悟和总结,并带剖析了大三期间应用的 vue-element-admin 这个 vuer 无所不知的后盾框架的动静路由权限管制原理。顺便附带本文实际 demo 地址: 基于后盾框架开发的 学生管理系统。

vue-router 源码剖析

首先浏览源码之前最好是将 Vuevue-router 的源码克隆下来,而后第一遍浏览倡议先跟着 官网文档 先走一遍根底用法,而后第二遍开始浏览源码,先理分明各层级目录的作用和抽出一些外围的文件进去,过一遍代码的同时写个小的 demo 边看边打断点调试,看不懂没关系,能够边看边参考一些总结的比拟好的文章,最初将比拟重要的原理过程依据本人的了解整理出来,而后画一画相干的常识脑图加深印象。

前置常识: flow 语法

JS 在编译过程中可能看不出一些荫蔽的谬误,但在运行过程中会报各种各样的 bug。flow 的作用就是编译期间进行动态类型查看,尽早发现错误,抛出异样。

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 官网,另外这种语法是相似于 TypeScript 的。

注册

咱们平时在应用 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 源码地址

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 源码地址

// 保留 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 源码地址。

构造函数
  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. 确定以后路由应用的 mode2. 实例化对应的 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

之前在 vueRouter 的构造函数中初始化了 macther, 本节将详细分析上面这句代码到底在做什么事件, 以及 match 办法在做什么源码地址。

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

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

export function createMatcher (
  routes: Array<RouteConfig>,
  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 源码地址

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // 记录所有的 path
  const pathList: Array<string> = oldPathList || []
  // 记录 path-RouteRecord 的 Map
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
   // 记录 name-RouteRecord 的 Map
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // 遍历所有的 route 生成对应映射表
  routes.forEach(route => {addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // 调整优先级
  for (let i = 0, l = pathList.length; i < l; 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 计算出一个新的门路并返回。

路由模式

vue-router 反对三种路由模式(mode):hashhistoryabstract,其中 abstract 是在非浏览器环境下应用的路由模式源码地址。

这一部分在后面初始化 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 反对三种路由模式,hashhistoryabstract。默认为 hash , 如果以后浏览器不反对history 则会做降级解决,而后实现 history 的初始化。

路由切换


切换 url 次要是调用了 push 办法,上面以哈希模式为例,剖析 push 办法实现的原理。push 办法切换路由的实现原理 源码地址

首先在 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 推历史栈当中。

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

源码地位

初始化 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

vue-routerinstall 时全局注册了两个组件一个是 router-view 一个是 router-link,这两个组件都是典型的函数式组件。源码地址

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 <a> child and apply listener and href
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> 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. 前端再依据以后的角色计算出相应的路由表拼接到惯例路由表前面。

登录生成动静路由全过程

理解 如何管制动静路由之后,上面是一张全过程流程图

前端在 beforeEach 中判断:

  • 缓存中存在 JWT 令牌

    • 拜访/login: 重定向到首页 /
    • 拜访 /login 以外的路由: 首次拜访,获取用户角色信息,而后生成动静路由,而后拜访以 replace 模式拜访 /xxx 路由。这种模式用户在登录之后不会在 history 寄存记录
  • 不存在 JWT 令牌

    • 路由在白名单中: 失常拜访 /xxx 路由
    • 不在白名单中: 重定向到 /login 页面

联合框架源码剖析

上面联合 vue-element-admin 的源码剖析该框架中如何解决路由逻辑的。

路由拜访逻辑剖析

首先能够定位到和入口文件 main.js 同级的 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 就去查看路由是否在白名单(任何状况都能拜访的路由),在的话就拜访,否则重定向回登录页面。

上面是通过全局守卫后路由变动的截图

联合 Vuex 生成动静路由

上面就是剖析这一步 const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 是怎么把路由生成进去的。源码地址

首先 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)
  }

动静路由生成全过程

总结

  • 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 源码 (万字长文)

vuejs 源码解析

招贤纳士

政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。

如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com

退出移动版