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.pswp.cn/news/276901.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/276901.shtml
英文地址,請注明出處:http://en.pswp.cn/news/276901.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MessagePack Java 0.6.X List, Map 對象的序列化和反序列化

為了序列化原生的容器對象例如 List 和 Map 對象,你必須使用 Template。 Template 對象是 serializer 和 deserializer 的配對。例如,為了序列化一個 List 對象,在 List 對象中 Integer 對象為元素,你可以使用下面的方法來創建一…

世界領先的界面設計公司: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,從顏色,到外形,到元素,無一例外,充滿了中國風的味道,可謂是中國風中的典型。同一風…

大家放松下,仿《大腕》經典對白

仿《大腕》經典對白: 一定要找那最流行的框架, 用功能最強大編輯器, 做就要做最復雜的系統, 輕量級的絕對不行, 框架最簡單也得是SPRING&…

MySQL-8.0.12源碼安裝實例

1、通過官網下載對應的版本后,通過FTP上傳至云服務器的/usr/local/src 目錄 2、解壓縮文件 [rootJSH-01 src]# ls mysql-boost-8.0.12.tar.gz [rootJSH-01 src]# tar zxvf mysql-boost-8.0.12.tar.gz [rootJSH-01 src]# ls mysql-8.0.12 mysql-boost-8.0.12.tar.gz…

python3常用模塊_Python3 常用模塊

一、time與datetime模塊 在Python中,通常有這幾種方式來表示時間: 時間戳(timestamp):通常來說,時間戳表示的是從1970年1月1日00:00:00開始按秒計算的偏移量。我們運行“type(time.time())”,返回的是float類型。 格式…

Windows下的HEAP溢出及其利用

Windows下的HEAP溢出及其利用 作者: isno 一、概述 前一段時間ASP的溢出鬧的沸沸揚揚,這個漏洞并不是普通的堆棧溢出,而是發生在HEAP中的溢出,這使大家重新認識到了Windows下的HEAP溢出的可利用性。其實WIN下的HEAP溢出比Linux和SOLARIS下面的…

地方政府不愿房價下跌 救市或化解房地產調控

地方政府不愿房價下跌 "救市"或化解房地產調控 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,結果尷尬了,提示我…

C語言程序設計 練習題參考答案 第八章 文件(2)

/* 8.8從文件ex88_1.txt中取出成績,排序后,按降序存放EX88_2.TXT中 */ #include "stdio.h" #define N 10 struct student { int num; char name[20]; int score[3]; /*不能使用float*/ float average; }; void sort(struc…

語法上的小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…

apache配置文件詳解與優化

apache配置文件詳解與優化 一、總結 一句話總結&#xff1a;結合apache配置文件中的英文說明和配置詳解一起看 1、apache模塊配置用的什么標簽&#xff1f; IfModule 例如&#xff1a; <IfModule dir_module>DirectoryIndex index.html 索引文件 首頁文件&#xff08;首頁…

帆軟報表(finereport)單元格函數,OP參數

單元格模型&#xff1a;單元格數據和引用&#xff1a;數據類型、實際值與顯示值、單元格支持的操作單元格樣式&#xff1a;行高列寬、隱藏行列、自動換行、上下標、文字豎排、大文本字段分頁時斷開、標識說明、格式刷單元格Web屬性&#xff1a;web顯示、web編輯風格、控件實際值…