第 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