用addRoutes實現動態路由

原文地址:前端路上,轉載請註明出處。

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

想當然的實現方案

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

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'));
//用此數據循環菜單
..

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

動態路由的坑

第一個坑是,如果你將這套邏輯實現之後會發現打開應用看到的第一個頁面是404,這是因爲啓動服務後將默認打開首頁’/‘,然而初始路由中沒有這個路徑,因此根據路由規則跳轉到了404。我們希望結果當然是跳轉到’/login’,因此需要對這種情況做判斷,在用戶登錄之前所有請求都要指向’/login’,這個判斷可以在before鉤子裏做也可以在根組件裏做,建議做在根組件的created回調裏,核心代碼大概這樣:

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

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

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裏的權限,再刷新一下頁面就能突破前端路由控制了,非常的不靠譜。

改進方案

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

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

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

首先是菜單,之前通過$router.options.routes訪問路由數據實現動態菜單,但這個數據不是響應式的,無法追蹤動態路由的變化,因此我們需要將得到的導航菜單數據存到sessionStorage或Vuex裏實現數據共享。

資源權限控制也受到很大的影響,實現較爲細緻的權限控制需要一個自定義權限驗證指令和一個全局驗證方法,之前的方案裏權限是在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貌似自帶惰性效果,可以先註冊後實現,具體原因我也不太明白,如過有大牛路過,希望能留下答案。

後記

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章