之前在基于Vue實現后臺系統權限控制一文中提到路由權限的實現思路,因為不喜歡在每次路由跳轉的before鉤子里做判斷,所以在初始化Vue實例前對路由做了篩選,再用實際路由初始化Vue實例,代價是登錄頁需要從Vue實例中獨立出來,實現上倒沒什么問題,不過這種做法需要在登錄和首頁之間通過url跳轉,感覺總是不太”優雅”,實際上只要能在登錄后動態修改當前實例的路由就行了,之前確實沒辦法,但vue-router 2.2版本新增了一個router.addRoutes(routes)方法,讓動態路由得以實現。
想當然的實現方案
用動態路由實現路由權限控制貌似是一個完美的方案,初始路由只有登錄和404,登錄后動態添加可用路由,同時將菜單數據保存到Vuex或本地用于實現動態菜單,關鍵節點大致如下:
//初始路由:[{ 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回調里,核心代碼大概這樣:
let isLogin = sessionStorage.getItem('user');if(!isLogin){ return this.$router.push({path:'/login'});}
這時候已經可以順利登錄了,登錄后很快就會發現第二個坑,手動刷新頁面又會跳到404,這是因為刷新會導致Vue重新實例化,路由也恢復到了初始路由,于是當前路徑又被重定向到了404,這個問題的根源是可用路由沒有實現持久化,那么可以通過將路由數據存sessionStorage來解決,實例化之前如果檢測到本地路由就直接合并路由,像這樣:
//檢測本地路由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的路由從初始路由中移除,動態更新路由時再把這個配置加進去,如下:
let userPath = ...//我們的動態路由//注入時拼接404處理路由this.$router.addRoutes(userPath.concat([{ path: '*', redirect: '/404'}]));
這樣就解決了刷新問題,后面還有幾個小問題就簡單了。
首先是菜單,之前通過$router.options.routes訪問路由數據實現動態菜單,但這個數據不是響應式的,無法追蹤動態路由的變化,因此我們需要將得到的導航菜單數據存到sessionStorage或Vuex里實現數據共享。
資源權限控制也受到很大的影響,實現較為細致的權限控制需要一個自定義權限驗證指令和一個全局驗證方法,之前的方案里權限是在Vue實例化之前獲取的,所以可以很方便的拿到權限后實現驗證方法,然后用驗證方法實現自定義指令,再將方法全局混合進Vue,然后實例化,這樣實例中的 所有組件都可以使用自定義指令和驗證方法;但現在的方案是先實例化再獲取權限,實例化之前根本沒有權限數據,所以自定義指無法實現,等拿到權限后實現了驗證方法,卻無法再全局混合了。
這個問題最后也解決了,但解決方案就徹底的”有辱斯文”了,首先是全局方法的實現,直接這么做:
Vue.prototype.has = function(){ ...}
使用方式跟全局混合的方法完全一樣。
自定義指令的實現本來很頭疼,因為全局指令只能在實例化之前實現,但那時候又確實沒有權限,不過既然驗證方法這么做的話,指令倒是也順便解決了,像這樣:
//權限指令Vue.directive('has', { bind: function(el, binding) { if (!Vue.prototype.has(binding.value)) { el.parentNode.removeChild(el); } }});
神奇的prototype貌似自帶惰性效果,可以先注冊后實現,具體原因我也不太明白,如過有大牛路過,希望能留下答案。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。
新聞熱點
疑難解答