通過路由也就是菜單來管理權限的方式,通常分為兩種:
1. 前端控制
靜態路由,前端將路由寫死,登錄的時候根據返回的角色權限(level等級),來動態展示路由
2. 后端控制
動態路由,后台返回角色對應的權限路由,前端通過調用接口結合導航守衛進行路由添加
先說下第一種方式,前端控制的實現思路:
前端將路由寫死,也就是將所有的路由映射表都拿到前端來維護,和我們不做菜單權限管理時一樣,在router.js里面配置好所有的路由
然后在登錄的時候獲取角色對應的level存入storage中,在側邊菜單欄組件的cretaed中根據level處理路由,給匹配的路由添加hidden屬性
最后我們用處理后的數據渲染菜單欄
這種方式存在比較明顯的缺點,也是router.js寫死的缺點,那就是:
我們如果記住了path,可以直接在瀏覽器網址欄中手動輸入path,然后回車就可以看到任何頁面。
再重點說下第二種方式,后端控制的實現思路,這也當前是常用到的一種權限控制方式:
這種方式我們通常會將一些不需要權限的路由寫死在router.js之中,比如login和404頁面等
routes: [{ path: '/login', name: 'login', component: () => import('./views/Login/index'), hidden: true, meta: { title: '登陸' } }]
而其他的路由有兩種處理方式,要么全部由我們的后端返回,
要么定義一個routerList.js將組件資源放到里面,然后通過后端返回的路由去做匹配,將匹配成功的通過addRouters添加到路由中
routerList.js
import LayOut from '@/components/layOut/index' export const mockRouter = [ { path: '/activeIssue', component: LayOut, redirect: '/activeIssue/index', meta: { title: '活動發布', }, children: [ { path: 'specialList', name: 'activeIssue_specialList', component: () => import('@/views/activeIssue/specialList.vue'), meta: { title: '專場活動列表', } }, { name: 'activeIssue_banner', path: 'banner', component: () => import('@/views/activeIssue/banner.vue'), meta: { title: '首頁banner', } } ] }]
這里再說一下我們的項目結構,通常會是在app.vue中有一個router-viev用來渲染我們的登錄頁面和主頁面,
我們定義一個Layout組件作為主頁面,而在我們的Layout組件中再分為側邊菜單欄和渲染對應page的router-view,這個router-view也就是我們渲染大多頁面的容器了。
這里的Layout就是上面routerList.js引入的Layout組件。
而動態添加路由這個方法要寫到導航守衛beforeEach這個鈎子函數中,這樣可以避免寫在登錄后的頁面刷新丟失后台返回的路由表。
導航守衛的意思是我路由跳轉到下個頁面之前要做些什么,就是說我們登錄后會跳到主頁面,在進到這個頁面之前我們需要將后端請求回來的路由表進行二次封裝,
根據返回的路由與我們前端的routerList.js去做匹配,需要做些什么根據需要來定,最后將處理后的路由通過router的addRoutes方法添加到我們的路由中,
之后再進入到我們的主頁面,通過主頁面進入對應的page頁面,也就是說權限控制(菜單權限)需要在進入主頁面之前完成。
總結大致步驟:beforEach攔截路由 => 接口獲取路由 => vuex保存路由 => 路由匹配處理 => 添加路由 => 跳轉進入主頁面
定義一個permisson.js來做路由處理:
import router from '@/router' //引入路由 import NProgress from 'nprogress' // progress bar import store from '@/store' //引入狀態機 import 'nprogress/nprogress.css' // 獲取token import { getSession } from '@/utils/saveStroage' import { mockRouter } from '@/assets/js/routerList.js'; //本地routerList import LayOut from '@/components/layOut/index' //LayOut組件 import errorPage from '@/views/error/404.vue' import Vue from 'vue'; let saveMenu = []; let activeIssue = {}; function clearHttpRequestingList(){ //清除cancleToken請求列表 if (Vue.$httpRequestList.length > 0) { Vue.$httpRequestList.forEach((item) => { item() }) Vue.$httpRequestList = [] } } // 將后台返回的菜單進行篩選,返回對應的菜單名稱,組成一維數組 function formatMenu(arr) { for (let i = 0; i < arr.length; i++) { if (arr[i].children) { saveMenu.push(arr[i].menuName) formatMenu(arr[i].children); } else { saveMenu.push(arr[i].menuName) } } return saveMenu; } // 遞歸篩選路由 function screenRoute(userRouter = [], allRouter = []) { var realRoutes = allRouter .filter(item => { if (item.meta && item.meta.title != '活動發布') { return userRouter.includes(item.meta.title) } else { return item } }) .map(item => ({ ...item, children: item.children ? screenRoute(userRouter, item.children) : null })) return realRoutes } // 添加路由 function addRout(arr) { arr.filter(t => { // 一級菜單 if (t.level == 'levelOne' && !t.children) { t.component = LayOut; t.type = 'One' t.redirect = t.path + '/index'; t.children = [{ path: 'index', meta: { title: t.menuName }, component: () => import('@/views' + t.path + '/index'), }] } // 多級菜單 else { // 當等級為一級時,添加title及引入模塊 if (t.level == 'levelOne') { t.component = LayOut; t.meta = { title: t.menuName } t.redirect = t.children[0].path } // 反之添加路徑 else { t.component = () => import('@/views' + t.path); t.meta = { title: t.menuName } } } if (t.children && t.type != 'One') { addRout(t.children); } }) return arr; } // 路由替換,所有具有詳情的路由替換成固定路由 function replaceDetails(arr) { mockRouter.filter(t => { arr.filter((r, index) => { if (t.path == r.path) { arr[index].children = [ ...arr[index].children, ...t.children ]; } }) }) return arr; } router.beforeEach(async (to, from, next) => { clearHttpRequestingList(); // start progress bar NProgress.start() document.title = to.meta.title; let token = getSession('token') || ''; const menuList = store.state.menuList && store.state.menuList.length > 0; if (token) { //已登錄 if (to.path == '/login') { next({ path: '/' //進入登錄頁面 }) NProgress.done() } else { if (menuList) { //已獲取路由 next(); } else { //未獲取路由 try { let { data } = await store.dispatch('getUserMenu'); //接口獲取路由 let addRouter = addRout(data); //路由處理成符合routes選項要求的數組(addRoutes的參數必選是一個符合 routes 選項要求的數組) replaceDetails(addRouter); //按需要接口返回路由和本地routerList.js做匹配處理 let t = [{ path: '/', name: 'Home', hidden: true, redirect: addRouter[0].redirect, }, { name: 'error', hidden: true, meta: { title: '404' }, path: '/404', component: errorPage }, { path: "*", hidden: true, redirect: "/404" }]; addRouter.push(...t); //根據vue-router中的匹配優先級來最后addRoutes 404和*這個頁面,這樣就可以在訪問非權限或不存在頁面時直接到達404頁面而非空頁面。 router.options.routes = addRouter; //手動添加,解決在addroutes后,router.options.routes不會更新的問題 // console.log('addRouter',addRouter) router.addRoutes(addRouter); //添加路由 store.commit('getMenuList', data); next({ ...to, replace: true }) } catch (error) { await store.dispatch('resetToken'); next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { //未登錄 if (to.name == 'login') { next(); } else { next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })
在main.js中引入promisson.js
import Router from 'vue-router' import store from './store' import './assets/js/permisson.js'
通過這種方式來控制權限,能夠很好的解決我們在瀏覽器導航欄改path,進入對應頁面的問題,
這樣操作會回歸到導航守衛中,當我addRoutes后如果沒有匹配到這個值就跳轉到404頁面。