大家在做后臺管理系統(tǒng)時一般都會涉及到菜單的權(quán)限控制問題睬魂。當(dāng)然解決問題的方法無非兩種——前端控制和后端控制糠馆。我們公司這邊的產(chǎn)品迭代速度較快,所以我們是從前端控制路由迭代到后端控制路由。下面我會分別介紹這兩種方法的優(yōu)缺點(diǎn)以及如何實(shí)現(xiàn)(不熟悉vue-router API的同學(xué)可以先去官網(wǎng)看一波API哈)。
我先簡單說下項(xiàng)目的需求:如下圖所示台颠,有一級菜單和二級菜單,然后不同的人登錄進(jìn)去會展示不同的菜單勒庄。
前端控制路由的思路:將所有的路由映射表都拿到前端來維護(hù)串前,就是我的router.js里面將所有的菜單path與對應(yīng)的components全部寫進(jìn)去,后面我會提到全部寫進(jìn)去的弊端实蔽。然后我的左側(cè)菜單寫成一個組件(sidebar.vue)荡碾,在這個組件里寫一份類似這樣的data數(shù)據(jù),然后通過登錄時獲取的level值來給data中固定的菜單加hidden局装,然后前端就根據(jù)hidden展示菜單坛吁。
// 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: '新增閃屏廣告'
}
}
]
}
];
下面是菜單組件的偽代碼
// 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 == "車機(jī)管理"){
value.hidden = true;
value.children.forEach(function(value){
if(value.title=="車機(jī)解綁"){
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>
雖然說這樣可以實(shí)現(xiàn)權(quán)限功能,但有兩個問題铐尚。
1. session里存的是level拨脉,我們可以打開瀏覽器控制臺人為控制level,這樣就失去了權(quán)限的意義宣增。
2. 我們?nèi)绻涀×藀ath玫膀,可以直接在瀏覽器網(wǎng)址欄中手動輸入path爹脾,然后回車就可以看到任何頁面灵妨。這也是前端router.js寫死所有路由的弊端货抄。
在這里面前端只是通過后端傳回的level來給router顯示/隱藏桨武,這樣前端維護(hù)整個路由是比較復(fù)雜的而且是有重大隱患的凉蜂。
現(xiàn)在呢我們來講講后端控制路由窿吩。先從操作流程來說煌往,我們這邊加入了一個dashboard中間頁,這個頁面只展示不同level下的一級路由曲管,通過點(diǎn)擊相應(yīng)的一級路由進(jìn)到對應(yīng)的Page頁面,該page頁面也只展示相對應(yīng)的所有的二級路由。
這里面出現(xiàn)了兩個個新的概念叫 “動態(tài)添加路由”和“導(dǎo)航守衛(wèi)”,就是我前端router.js中只寫所有人可以訪問的路由表,比如login和404頁面等。其他所有的組件資源全部寫到一個新的components.js文件中锁保,然后通過后端返回的menuData去映射符合components.js中的key吴菠,如果有對應(yīng)的做葵,就把它動態(tài)添加到router中,通過addRoutes添加瘫筐。動態(tài)添加路由這個方法要寫到導(dǎo)航守衛(wèi)beforeEach這個鉤子函數(shù)中策肝。導(dǎo)航守衛(wèi)的意思是我路由跳轉(zhuǎn)到下個頁面之前要做些什么。就是說我們登錄后會跳到dashboard頁面酝枢,在進(jìn)到這個頁面之前我們需要將后端請求回來的menuData進(jìn)行二次封裝坦康,把他根據(jù)權(quán)限返回回來的data與我們前端components.js去做map匹配古胆,將最終的數(shù)據(jù)通過addRoutes來push到我們的路由中,之后才能進(jìn)到我們的dashborad頁面,再通過dashborad頁面進(jìn)到對應(yīng)的page頁面颊乘,就是說我們把所有的權(quán)限控制全在dashboard頁面進(jìn)入之前就做完了浙值。這里面還有一個小的優(yōu)化的點(diǎn):當(dāng)我們通過前面說的瀏覽器菜單欄訪問到非權(quán)限頁面或者不存在的頁面時识啦,需要根據(jù)vue-router中的匹配優(yōu)先級來最后addRoutes 404和*這個頁面冕茅,這樣就可以直接到達(dá)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 偽代碼 只保留具體的相關(guān)邏輯
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;
//使用鉤子函數(shù)對路由進(jìn)行權(quán)限跳轉(zhuǎn)
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請求數(shù)據(jù)
// 一級菜單與二級菜單的區(qū)別是一級菜單帶有component這個值庸疾,比如下面的短信管理就是只有一級菜單
{
"errno": 0,
"errmsg": "獲取權(quán)限成功",
"result": [
{
"index": "1",
"title": "廣告管理",
"icon": "iconfont icon-guanggao",
"children": [
{
"index": "splashScreen",
"icon": "",
"title": "閃屏關(guān)羽配置",
"path": "/home/splashAdverse",
"component": "splashAdverse",
"isShow": true
},
{
"index": "addSplashScreen",
"icon": "",
"title": "新增關(guān)羽廣告",
"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{
// 二級菜單的數(shù)據(jù)結(jié)構(gòu)中只在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'));
// 通過當(dāng)前router的path來map對應(yīng)的整個路由數(shù)組
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;
}
}
})
// 去掉數(shù)組中的undefined、null 等空值 假值
this.routes = this.bouncer(routes);
}
}
</script>
通過這種方式來控制權(quán)限届慈,我們?nèi)绻跒g覽器控制臺改了session中的level或者在瀏覽器導(dǎo)航欄改path徒溪,都會回歸到導(dǎo)航守衛(wèi)中,就是發(fā)請求重新獲取menuData金顿,當(dāng)我addRoutes后如果沒有匹配到這個值就回到404臊泌,當(dāng)然通過改level也不會達(dá)到修改權(quán)限的控制,因?yàn)槲覀兪莿討B(tài)獲取路由揍拆,不是之前的前端控制路由渠概。
目前為止,我感覺通過后端控制權(quán)限這種實(shí)現(xiàn)方式應(yīng)該是最理想的一種吧嫂拴,當(dāng)然大家有更好的方法或者對此文有任何問題播揪,歡迎大家留言哈。