第 72 篇原创好文~ 本文首发于政采云前端团队博客:浅析 vue-router 源码和动态路由权限分配 javascript
上月立过一个 flag,看完 vue-router
的源码,可到后面逐渐发现 vue-router
的源码并非像不少总结的文章那么容易理解,阅读过你就会发现里面的不少地方都会有多层的函数调用关系,还有大量的 this 指向问题,并且会有不少辅助函数须要去理解。但仍是坚持啃下来了(固然还没看完,内容是真的多),下面是我在政采云(实习)工做闲暇时间阅读源码的一些感悟和总结,并带分析了大三时期使用的 vue-element-admin 这个 vuer 无所不知的后台框架的动态路由权限控制原理。顺便附带本文实践 demo 地址: 基于后台框架开发的 学生管理系统。前端
首先阅读源码以前最好是将 Vue
和 vue-router
的源码克隆下来,而后第一遍阅读建议先跟着 官方文档 先走一遍基础用法,而后第二遍开始阅读源码,先理清楚各层级目录的做用和抽出一些核心的文件出来,过一遍代码的同时写个小的 demo 边看边打断点调试,看不懂不要紧,能够边看边参考一些总结的比较好的文章,最后将比较重要的原理过程根据本身的理解整理出来,而后画一画相关的知识脑图加深印象。vue
JS 在编译过程当中可能看不出一些隐蔽的错误,但在运行过程当中会报各类各样的 bug。flow 的做用就是编译期间进行静态类型检查,尽早发现错误,抛出异常。java
Vue
、Vue-router
等大型项目每每须要这种工具去作静态类型检查以保证代码的可维护性和可靠性。本文所分析的 vue-router
源码中就大量的采用了 flow 去编写函数,因此学习 flow 的语法是有必要的。node
首先安装 flow 环境,初始化环境git
npm install flow-bin -g flow init
在 index.js
中输入这一段报错的代码github
/*@flow*/ function add(x: string, y: number): number { return x + y } add(2, 11)
在控制台输入 flow ,这个时候不出意外就会抛出异常提示,这就是简单的 flow 使用方法。正则表达式
具体用法还须要参考 flow官网,另外这种语法是相似于 TypeScript 的。vue-router
咱们平时在使用 vue-router
的时候一般须要在 main.js
中初始化 Vue
实例时将 vue-router
实例对象当作参数传入vuex
例如:
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(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 } }
能够看到 Vue
的 use
方法会接受一个 plugin
参数,而后使用 installPlugins
数组保存已经注册过的 plugin
。 首先保证 plugin
不被重复注册,而后将 Vue
从函数参数中取出,将整个 Vue
做为 plugin
的install
方法的第一个参数,这样作的好处就是不须要麻烦的另外引入 Vue
,便于操做。 接着就去判断 plugin
上是否存在 install
方法。存在则将赋值后的参数传入执行 ,最后将全部的存在 install
方法的 plugin
交给 installPlugins
维护。
了解清楚 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
,最后注册 RouterView
和 RouterLink
,这就是整个 install
的过程。
Vue.use(plugin)
实际上在执行 plugin上的 install
方法,insatll
方法有个重要的步骤:
mixin
在组件中混入 beforeCreate
, destory
这俩个生命周期钩子beforeCreate
这个钩子进行初始化。router-view
,router-link
组件接着就是这个最重要的 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. 肯定当前路由使用的 mode
2. 实例化对应的 history
对象。
接着完成实例化 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
进行路由过渡。
以前在 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
返回的实例。
下面这句代码是在建立一张 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 }
废了这么大劲将 pathList
和 pathMap
和 nameMap
抽出来是为啥呢?
首先 pathList
是记录路由配置全部的 path
,而后 pathMap
和 nameMap
方便咱们传入 path
或者 name
快速定位到一个 record
,而后辅助后续路径切换计算路由的。
这是在 vue2.2.0
以后新添加的 api
,或许不少状况路由并非写死的,须要动态添加路由。有了前面的 createRouteMap
的基础上咱们只须要传入 routes
便可,他就能在原基础上修改
function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) }
而且看到在 createMathcer
最后返回了这个方法,因此咱们就可使用这个方法
return { match, addRoutes }
function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { ... }
接下来就是 match
方法,它接收 3 个参数,其中 raw
是 RawLocation
类型,它能够是一个 url
字符串,也能够是一个 Location
对象;currentRoute
是 Route
类型,它表示当前的路径;redirectedFrom
和重定向相关。match
方法返回的是一个路径,它的做用是根据传入的 raw
和当前的路径 currentRoute
计算出一个新的路径并返回。至于他是如何计算出这条路径的,能够详细看一下如何计算出location
的 normalizeLocation
方法和 _createRoute
方法。
createMatcher
: 根据路由的配置描述创建映射表,包括路径、名称到路由 record
的映射关系, 最重要的就是 createRouteMap
这个方法,这里也是动态路由匹配和嵌套路由的原理。addRoutes
: 动态添加路由配置match
: 根据传入的 raw
和当前的路径 currentRoute
计算出一个新的路径并返回。vue-router
支持三种路由模式(mode):hash
、history
、abstract
,其中 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
支持三种路由模式,hash
、history
和abstract
。默认为 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
底层调用了浏览器原生的 history
的 pushState
和 replaceState
方法,不是 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
计算出 url
为http://localhost:8080/#/
,最后执行 pushState(url,true)
,就大功告成了!
hash
模式的 push
方法会调用路径切换方法 transitionTo
,接着在回调函数中调用pushHash
方法,这个方法调用的 pushState
方法底层是调用了浏览器原生 history
的方法。push
和 replace
的区别就在于一个将 url
推入了历史栈,一个没有,最直观的体现就是 replace
模式下浏览器点击后退不会回到上一个路由去 ,另外一个则能够。
vue-router
在 install
时全局注册了两个组件一个是 router-view
一个是 router-link
,这两个组件都是典型的函数式组件。源码地址
首先在 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 }) })
分析几个重要的部分:
active
路由样式router-link
之因此能够添加 router-link-active
和 router-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 是十分困难的的。
目前主流的路由权限控制的方式是:
token
保存到本地,接着前端会携带 token
再调用获取用户信息的接口获取当前用户的角色信息。了解 如何控制动态路由以后,下面是一张全过程流程图
前端在 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
就去检查路由是否在白名单(任何状况都能访问的路由),在的话就访问,不然重定向回登陆页面。
下面是通过全局守卫后路由变化的截图
下面就是分析这一步 const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
是怎么把路由生成出来的。源码地址
首先 vue-element-admin
中路由是分为两种的:
// 无需校验身份路由 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
读取 asyncRoutes
和 constantRoutes
以后首先判断当前角色是不是 admin
,是的话默认超级管理员可以访问全部的路由,固然这里也能够自定义,不然去过滤出路由权限路由表,而后保存到 Vuex
中。 最后将过滤以后的 asyncRoutes
和 constantRoutes
进行合并。
过滤权限路由的源码以下:
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
方法,注入生命周期钩子初始化beforeCreate
传入 router
实例时,执行 init
函数,而后执行 history.transitionTo
路由过渡routes
配置建立对应的 pathMap
和 nameMap
,能够根据传入的位置和路径计算出新的位置并匹配对应的 record
vueRouter
时完成匹配,若是浏览器不支持则会降级pushState
和 replaceState
方法$route.match
控制路由对应的组件的渲染状况,而且支持嵌套。to
来决定点击事件跳转的目标路由组件,而且支持渲染成不一样的 tag
,还能够修改激活路由的样式。权限控制动态路由部分
/login
特殊状况,不存在则判断白名单而后走对应的逻辑router.js
定义的两种路由。判断当前身份是不是管理员,是则直接拼接,不然须要过滤出具有权限的路由,最后拼接到常规路由后面,经过 addRoutes
追加。或许阅读源码的做用不能像一篇开发文档同样直接立马对平常开发有所帮助,可是它的影响是长远的,在读源码的过程当中均可以学到众多知识,相似闭包、设计模式、时间循环、回调等等 JS 进阶技能,并稳固并提高了你的 JS 基础。固然这篇文章是有缺陷的,有几个地方都没有分析到,好比导航守卫实现原理和路由懒加载实现原理,这一部分,我还在摸索当中。
若是一味的死记硬背一些所谓的面经,或者直接死记硬背相关的框架行为或者 API ,你很难在遇到比较复杂的问题下面去快速定位问题,了解怎么去解决问题,并且我发现不少人在使用一个新框架以后遇到点问题都会立马去提对应的 Issues
,以致于不少流行框架 Issues
超过几百个或者几千个,可是许多问题都是由于咱们并未按照设计者开发初设定的方向才致使错误的,更多都是些粗枝大叶形成的问题。
参考文章
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com