用addRoutes實現動態路由

原文轉自前端路上,轉載請註明出處。前端

 

以前在基於Vue實現後臺系統權限控制一文中提到路由權限的實現思路,由於不喜歡在每次路由跳轉的before鉤子裏作判斷,因此在初始化Vue實例前對路由作了篩選,再用實際路由初始化Vue實例,代價是登陸頁須要從Vue實例中獨立出來,實現上倒沒什麼問題,不過這種作法須要在登陸和首頁之間經過url跳轉,感受老是不太」優雅」,實際上只要能在登陸後動態修改當前實例的路由就好了,以前確實沒辦法,但vue-router 2.2版本新增了一個router.addRoutes(routes)方法,讓動態路由得以實現。vue

想固然的實現方案

用動態路由實現路由權限控制貌似是一個完美的方案,初始路由只有登陸和404,登陸後動態添加可用路由,同時將菜單數據保存到Vuex或本地用於實現動態菜單,關鍵節點大體以下:ios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//初始路由:
[{
path: '/login',
name: 'login',
component: (resolve) => require(['../views/common/404.vue'], resolve)
}, {
path: '/404',
name: '404',
component: (resolve) => require(['../views/common/404.vue'], resolve)
}, {
path: '*',
redirect: '/404'
}]

//登陸邏輯
let vm = this;
axios.get('/login', vm.user).then((res) => {
let extendsRoutes = filterRoutes(res.menus);
<!--
//假設獲得的可用路由以下
[{
path: '/',
name: '首頁',
component: (resolve) => require(['../views/index.vue'], resolve),
children: [{
path: '/menus',
name: '菜單管理',
component: (resolve) => require(['../views/menus.vue'], resolve)
}, {
path: '/resources',
name: '資源管理',
component: (resolve) => require(['../views/resources.vue'], resolve)
}]
}]-->
//存菜單
sessionStorage.setItem('menus',JSON.stringify(extendsRoutes[0].children));
//動態添加路由
vm.$router.addRoutes(extendsRoutes);
//跳轉到應用界面
vm.$router.push({path:'/'});
})

//首頁獲取菜單數據
this.menus = JSON.parse(sessionStorage.getItem('menus'));
//用此數據循環菜單
..

目前爲止看上去一切順利,然而前方有坑。vue-router

動態路由的坑

第一個坑是,若是你將這套邏輯實現以後會發現打開應用看到的第一個頁面是404,這是由於啓動服務後將默認打開首頁’/‘,然而初始路由中沒有這個路徑,所以根據路由規則跳轉到了404。咱們但願結果固然是跳轉到’/login’,所以須要對這種狀況作判斷,在用戶登陸以前全部請求都要指向’/login’,這個判斷能夠在before鉤子裏作也能夠在根組件裏作,建議作在根組件的created回調裏,核心代碼大概這樣:axios

1
2
3
4
let isLogin = sessionStorage.getItem('user');
if(!isLogin){
return this.$router.push({path:'/login'});
}

這時候已經能夠順利登陸了,登陸後很快就會發現第二個坑,手動刷新頁面又會跳到404,這是由於刷新會致使Vue從新實例化,路由也恢復到了初始路由,因而當前路徑又被重定向到了404,這個問題的根源是可用路由沒有實現持久化,那麼能夠經過將路由數據存sessionStorage來解決,實例化以前若是檢測到本地路由就直接合並路由,像這樣:session

1
2
3
4
5
6
7
8
9
10
11
//檢測本地路由
let localRoutes = sessionStorage.getItem('routes');
if(localRoutes){
router.addRoutes(JSON.parse(localRoutes));
}
//實例化
new Vue({
el: '#app',
router,
render: h => h(App)
});

理論上能夠,但實際操做要遠比上述代碼複雜,由於存在本地的只能是權限數據而不是真實路由,路由在存、取以前都要先根據權限匹配得到,過程仍是挺繁瑣的,並且必須依賴sessionStorage這種持久存儲,沒有其餘方法。問題就出在這個sessionStorage上,原則上權限只能在內存變量中流轉,不能直接暴露到用戶可操做的地方,試想只要用戶手動修改了sessionStorage裏的權限,再刷新一下頁面就能突破前端路由控制了,很是的不靠譜。app

改進方案

既然不能存本地,那就每次刷新都從新從服務端獲取,因此改進後的方案是本地存用戶token,每次刷新要憑token從服務端從新獲取用戶信息和權限,而後動態更新路由,獲取權限操做能夠跟登陸檢測一塊兒放在根組件的created回調中進行,確保訪問任何路徑都會先執行這一步,但由於獲取權限是異步操做,在此以前仍然會通過應用初始化,因此仍是會遇到404的問題,爲此咱們只需作一個小調整,將不匹配路徑(‘*’)跳404的路由從初始路由中移除,動態更新路由時再把這個配置加進去,以下:異步

1
2
3
4
5
6
let userPath = ...//咱們的動態路由
//注入時拼接404處理路由
this.$router.addRoutes(userPath.concat([{
path: '*',
redirect: '/404'
}]));

這樣就解決了刷新問題,後面還有幾個小問題就簡單了。ui

首先是菜單,以前經過$router.options.routes訪問路由數據實現動態菜單,但這個數據不是響應式的,沒法追蹤動態路由的變化,所以咱們須要將獲得的導航菜單數據存到sessionStorage或Vuex裏實現數據共享。this

資源權限控制也受到很大的影響,實現較爲細緻的權限控制須要一個自定義權限驗證指令和一個全局驗證方法,以前的方案裏權限是在Vue實例化以前獲取的,因此能夠很方便的拿到權限後實現驗證方法,而後用驗證方法實現自定義指令,再將方法全局混合進Vue,而後實例化,這樣實例中的 全部組件均可以使用自定義指令和驗證方法;但如今的方案是先實例化再獲取權限,實例化以前根本沒有權限數據,因此自定義指沒法實現,等拿到權限後實現了驗證方法,卻沒法再全局混合了。

這個問題最後也解決了,但解決方案就完全的」有辱斯文」了,首先是全局方法的實現,直接這麼作:

1
2
3
Vue.prototype.has = function(){
...
}

使用方式跟全局混合的方法徹底同樣。

自定義指令的實現原本很頭疼,由於全局指令只能在實例化以前實現,但那時候又確實沒有權限,不過既然驗證方法這麼作的話,指令卻是也順便解決了,像這樣:

1
2
3
4
5
6
7
8
//權限指令
Vue.directive('has', {
bind: function(el, binding) {
if (!Vue.prototype.has(binding.value)) {
el.parentNode.removeChild(el);
}
}
});

神奇的prototype貌似自帶惰性效果,能夠先註冊後實現,具體緣由我也不太明白,如過有大牛路過,但願能留下答案。

後記

生命不息,折騰不止啊,原本已經放棄的思路,捋着捋着居然捋順了,而後又花了大半天把原來多入口的項目改爲了單入口,雖然麻煩了一頓,但內心總算舒坦了。

相關文章
相關標籤/搜索