你們在作後臺管理系統時通常都會涉及到菜單的權限控制問題。固然解決問題的方法無非兩種——前端控制和後端控制。咱們公司這邊的產品迭代速度較快,因此咱們是從前端控制路由迭代到後端控制路由。下面我會分別介紹這兩種方法的優缺點以及如何實現(不熟悉vue-router API的同窗能夠先去官網看一波API哈)。html
我先簡單說下項目的需求:以下圖所示,有一級菜單和二級菜單,而後不一樣的人登陸進去會展現不一樣的菜單。前端
前端控制路由的思路:將全部的路由映射表都拿到前端來維護,就是個人router.js裏面將全部的菜單path與對應的components所有寫進去,後面我會提到所有寫進去的弊端。而後個人左側菜單寫成一個組件(sidebar.vue),在這個組件裏寫一份相似這樣的data數據,而後經過登陸時獲取的level值來給data中固定的菜單加hidden,而後前端就根據hidden展現菜單。vue
// router.js 僞代碼
const Login = r => require.ensure([],()=>r(require('../page/login/Login.vue')),'login'); const Home = r => require.ensure([],()=>r(require('../page/Home.vue')),'home'); const Forbidden = r => require.ensure([],()=>r(require('../page/403.vue')),'forbidden'); const NotFound = r => require.ensure([],()=>r(require('../page/404.vue')),'notfound'); const Dashboard = r => require.ensure([],()=>r(require('../page/dashboard/Dashboard.vue')),'dashboard'); const SplashScreen = r => require.ensure([],()=>r(require('../page/splashScreen/SplashScreen.vue')),'splashScreen'); const AddSplashScreen = r => require.ensure([],()=>r(require('../page/splashScreen/AddSplashScreen.vue')),'addSplashScreen'); const routes = [ { path: '/', redirect: '/login' },{ path: '/login', component: Login },{ path: '/404', component: NotFound },{ path: '/home', component: Home, redirect: '/home/splashScreen', children: [ { path: '/home/splashScreen', component: SplashScreen, meta: { title: '閃屏廣告配置' } },{ path: '/home/addSplashScreen', component: AddSplashScreen, meta: { title: '新增閃屏廣告' } } ] } ];
下面是菜單組件的僞代碼ajax
// sidebar.vue <template> <div class="sidebar"> <el-menu> ... </el-menu> </div> </template> <script> export default { data() { return { routes: [ { index: '1', title: '廣告管理', icon: 'iconfont icon-guanggao', children: [ { index: 'splashScreen', title: '閃屏廣告配置', children: [] }, ] }, { index: '2', title: '推送管理', icon:'iconfont icon-tuisongguanli-', } ] } }, methods: { getLevel(){ const level = sessionStorage.getItem('level'); if(level === '0'){ this.routes.forEach(function(value){ if(value.title == "車機管理"){ value.hidden = true; value.children.forEach(function(value){ if(value.title=="車機解綁"){ value.hidden = true; } }) } }) }else if(level === '1'){ this.routes.forEach(function(value){ value.hidden = true value.children.forEach(function(value){ value.hidden = true; }) }) } } }, created(){ this.getLevel(); } } </script>
雖說這樣能夠實現權限功能,但有兩個問題。vue-router
1. session裏存的是level,咱們能夠打開瀏覽器控制檯人爲控制level,這樣就失去了權限的意義。後端
2. 咱們若是記住了path,能夠直接在瀏覽器網址欄中手動輸入path,而後回車就能夠看到任何頁面。這也是前端router.js寫死全部路由的弊端。數組
在這裏面前端只是經過後端傳回的level來給router顯示/隱藏,這樣前端維護整個路由是比較複雜的並且是有重大隱患的。瀏覽器
如今呢咱們來說講後端控制路由。先從操做流程來講,咱們這邊加入了一個dashboard中間頁,這個頁面只展現不一樣level下的一級路由,經過點擊相應的一級路由進到對應的Page頁面,該page頁面也只展現相對應的全部的二級路由。session
這裏面出現了兩個個新的概念叫 「動態添加路由」和「導航守衛」,就是我前端router.js中只寫全部人能夠訪問的路由表,好比login和404頁面等。其餘全部的組件資源所有寫到一個新的components.js文件中,而後經過後端返回的menuData去映射符合components.js中的key,若是有對應的,就把它動態添加到router中,經過addRoutes添加。動態添加路由這個方法要寫到導航守衛beforeEach這個鉤子函數中。導航守衛的意思是我路由跳轉到下個頁面以前要作些什麼。就是說咱們登陸後會跳到dashboard頁面,在進到這個頁面以前咱們須要將後端請求回來的menuData進行二次封裝,把他根據權限返回回來的data與咱們前端components.js去作map匹配,將最終的數據經過addRoutes來push到咱們的路由中,以後才能進到咱們的dashborad頁面,再經過dashborad頁面進到對應的page頁面,就是說咱們把全部的權限控制全在dashboard頁面進入以前就作完了。這裏面還有一個小的優化的點:當咱們經過前面說的瀏覽器菜單欄訪問到非權限頁面或者不存在的頁面時,須要根據vue-router中的匹配優先級來最後addRoutes 404和*這個頁面,這樣就能夠直接到達404頁面而非空頁面。數據結構
// components.js 全部的頁面資源
const home = () => import('../page/Home.vue'); const splashScreen = () => import('../page/splashScreen/SplashScreen.vue'); const addSplashScreen = () => import('../page/splashScreen/AddSplashScreen.vue'); const editSplashScreen = () => import('../page/splashScreen/EditSplashScreen.vue'); export default { home, splashScreen, addSplashScreen, editSplashScreen, };
// router.js 看,只寫通用的頁面是否是很清爽 import Vue from 'vue'; import Router from 'vue-router'; Vue.use(Router); const Login = () => import('../page/login/Login.vue'); const Home = () => import('../page/Home.vue'); const Forbidden = () => import('../page/403.vue'); const Dashboard = () => import('../page/dashboard/Dashboard.vue'); const routes = [ { path: '/', redirect: '/login' },{ path: '/login', component: Login },{ path: '/403', component: Forbidden }, { path: '/dashboard', component: Dashboard, }, ]; export default new Router({ mode: 'history', routes: routes, base: __dirname, linkActiveClass: 'link-active' })
// main.js 僞代碼 只保留具體的相關邏輯 import routeMap from './router/component.js'; const NotFound = () => import('./page/404.vue'); const formatRoutes = function (routes, routeData) { if (!routeData) { routeData = { name: 'home', path: '/home', // 組件匹配成功的話才能夠訪問具體的頁面 component: routeMap['home'], children: [], }; } routes.length && routes.forEach(route => { if(route.component) { route.component = routeMap[route.component]; routeData.children.push({ path: route.path, name: route.index, component: route.component, meta: { title: route.title, }, }) } if (route.children && route.children.length) { formatRoutes(route.children, routeData); } }); return routeData; }; let isFetchRemote = true; //使用鉤子函數對路由進行權限跳轉 router.beforeEach((to, from, next) => { const username = sessionStorage.getItem('username'); if(!username && to.path !== '/login'){ next({path: '/login'}); } else if (isFetchRemote && to.path !== '/login') { ajaxPost('/resourceAPI/getMenuData').then(res =>{ if (res.status === 200 && res.data.errno === 0) { isFetchRemote = false; const menuData = res.data.result; localStorage.setItem('menudata', JSON.stringify(menuData)); const routeData = formatRoutes(menuData); resourceApp.$router.addRoutes([routeData].concat([ {name:'404',path:'/404',component:NotFound}, {path:'*',redirect: '/404'}])); resourceApp.$router.push({ path: to.path, query: to.query }); } else { isFetchRemote = true; } next(); }) .catch(err => { console.log(err); }); } else { next(); } }); const resourceApp = new Vue({ router, render: h => h(App) }).$mount('#app');
// menuData請求數據 // 一級菜單與二級菜單的區別是一級菜單帶有component這個值,好比下面的短信管理就是隻有一級菜單 { "errno": 0, "errmsg": "獲取權限成功", "result": [ { "index": "1", "title": "廣告管理", "icon": "iconfont icon-guanggao", "children": [ { "index": "splashScreen", "icon": "", "title": "閃屏關羽配置", "path": "/home/splashAdverse", "component": "splashAdverse", "isShow": true }, { "index": "addSplashScreen", "icon": "", "title": "新增關羽廣告", "path": "/home/addAdverse", "component": "addAdverse", "isShow": false }, ] }, { "index": "message", "title": "短信管理", "icon": "iconfont icon-duanxinguanli", "path": "/home/message", "component": "message", "children": [ { "index": "addMessage", "title": "新增短信", "icon": "", "path": "/home/addMessage", "component": "addMessage", "isShow": false } ] } ] }
而sidebar和dashboard這兩個組件都只須要經過session拿到後端的menudate就能夠。
// dashboard 僞代碼 <template> <div class="nav_list"> <div class="nav_list_item" v-for="item in navList" @click="goPage(item)"> <i :class="item.icon"></i> <h2>{{item.title}}</h2> </div> </div> </template> <script> created(){ const routeArr = JSON.parse(localStorage.getItem('menudata')); this.navList = routeArr; }, methods: { goPage(item){ // 只有一級菜單 if(item.component){ this.$router.push(item.path); }else{ // 二級菜單的數據結構中只在children中有path this.$router.push(item.children[0]['path']); } } } </script>
// sidebar 僞代碼 <script> export default { data() { return { routes: [], } }, methods: { bouncer(arr){ return arr.filter(function(val){ return !(!val || val === ""); }); } }, created(){ const menuData = JSON.parse(localStorage.getItem('menudata')); // 經過當前router的path來map對應的整個路由數組 let routes = menuData.map((item)=>{ // 只有一級路由 if(item.component && item.path == this.$route.path){ console.log(item) return item; }else{ if(item.children[0]['path'] == this.$route.path){ console.log(item) return item; } } }) // 去掉數組中的undefined、null 等空值 假值 this.routes = this.bouncer(routes); } } </script>
經過這種方式來控制權限,咱們若是在瀏覽器控制檯改了session中的level或者在瀏覽器導航欄改path,都會迴歸到導航守衛中,就是發請求從新獲取menuData,當我addRoutes後若是沒有匹配到這個值就回到404,固然經過改level也不會達到修改權限的控制,由於咱們是動態獲取路由,不是以前的前端控制路由。
目前爲止,我感受經過後端控制權限這種實現方式應該是最理想的一種吧,固然你們有更好的方法或者對此文有任何問題,歡迎你們留言哈。