第 72 篇原创好文~ 本文首发于政采云前端团队博客:浅析 vue-router 源码和动静路由权限调配
浅析 vue-router 源码和动静路由权限调配
背景
上月立过一个 flag,看完 vue-router
的源码,可到前面逐步发现 vue-router
的源码并不是像很多总结的文章那么容易了解,浏览过你就会发现外面的很多中央都会有多层的函数调用关系,还有大量的 this 指向问题,而且会有很多辅助函数须要去了解。但还是保持啃下来了 (当然还没看完,内容是真的多),上面是我在政采云(实习) 工作空闲工夫浏览源码的一些感悟和总结,并带剖析了大三期间应用的 vue-element-admin 这个 vuer 无所不知的后盾框架的动静路由权限管制原理。顺便附带本文实际 demo 地址: 基于后盾框架开发的 学生管理系统。
vue-router 源码剖析
首先浏览源码之前最好是将 Vue
和 vue-router
的源码克隆下来,而后第一遍浏览倡议先跟着 官网文档 先走一遍根底用法,而后第二遍开始浏览源码,先理分明各层级目录的作用和抽出一些外围的文件进去,过一遍代码的同时写个小的 demo 边看边打断点调试,看不懂没关系,能够边看边参考一些总结的比拟好的文章,最初将比拟重要的原理过程依据本人的了解整理出来,而后画一画相干的常识脑图加深印象。
前置常识: flow 语法
JS 在编译过程中可能看不出一些荫蔽的谬误,但在运行过程中会报各种各样的 bug。flow 的作用就是编译期间进行动态类型查看,尽早发现错误,抛出异样。
Vue
、Vue-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
}
}
能够看到 Vue
的 use
办法会承受一个 plugin
参数,而后应用 installPlugins
数组保留曾经注册过的 plugin
。首先保障 plugin
不被反复注册,而后将 Vue
从函数参数中取出,将整个 Vue
作为 plugin
的 install
办法的第一个参数,这样做的益处就是不须要麻烦的另外引入 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
,最初注册 RouterView
和 RouterLink
,这就是整个 install
的过程。
小结
Vue.use(plugin)
实际上在执行 plugin 上的 install
办法,insatll
办法有个重要的步骤:
- 应用
mixin
在组件中混入beforeCreate
,destory
这俩个生命周期钩子 - 在
beforeCreate
这个钩子进行初始化。 - 全局注册
router-view
,router-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. 确定以后路由应用的 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
之前在 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
}
废了这么大劲将 pathList
和 pathMap
和 nameMap
抽出来是为啥呢?
首先 pathList
是记录路由配置所有的 path
,而后 pathMap
和 nameMap
不便咱们传入 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 个参数,其中 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
模式下浏览器点击后退不会回到上一个路由去 , 另一个则能够。
router-view & router-link
vue-router
在 install
时全局注册了两个组件一个是 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-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
就去查看路由是否在白名单(任何状况都能拜访的路由),在的话就拜访,否则重定向回登录页面。
上面是通过全局守卫后路由变动的截图
联合 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
读取 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
办法,注入生命周期钩子初始化 - vueRouter: 当组件执行
beforeCreate
传入router
实例时, 执行init
函数,而后执行history.transitionTo
路由过渡 - matcher : 依据传入的
routes
配置创立对应的pathMap
和nameMap
, 能够依据传入的地位和门路计算出新的地位并匹配对应的record
- 路由模式: 路由模式在初始化
vueRouter
时实现匹配,如果浏览器不反对则会降级 - 路由 切换: 哈希模式下底层应用了浏览器原生的
pushState
和replaceState
办法 - 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