大家在做后臺(tái)管理系統(tǒng)時(shí)一般都會(huì)涉及到菜單的權(quán)限控制問題擦俐。當(dāng)然解決問題的方法無非兩種——前端控制和后端控制兽狭。我們公司這邊的產(chǎn)品迭代速度較快伊者,所以我們是從前端控制路由迭代到后端控制路由仔役。下面我會(huì)分別介紹這兩種方法的優(yōu)缺點(diǎn)以及如何實(shí)現(xiàn)(不熟悉vue-router API的同學(xué)可以先去官網(wǎng)看一波API哈)喜每。
我先簡單說下項(xiàng)目的需求:如下圖所示务唐,有一級(jí)菜單和二級(jí)菜單,然后不同的人登錄進(jìn)去會(huì)展示不同的菜單带兜。
前端控制路由的思路:將所有的路由映射表都拿到前端來維護(hù)枫笛,就是我的router.js里面將所有的菜單path與對應(yīng)的components全部寫進(jìn)去,后面我會(huì)提到全部寫進(jìn)去的弊端刚照。然后我的左側(cè)菜單寫成一個(gè)組件(sidebar.vue)刑巧,在這個(gè)組件里寫一份類似這樣的data數(shù)據(jù),然后通過登錄時(shí)獲取的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 == "國服上單"){
value.hidden = true;
value.children.forEach(function(value){
if(value.title=="國服關(guān)羽"){
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)限功能,但有兩個(gè)問題浑彰。
session里存的是level恭理,我們可以打開瀏覽器控制臺(tái)人為控制level,這樣就失去了權(quán)限的意義郭变。
我們?nèi)绻涀×藀ath蚯斯,可以直接在瀏覽器網(wǎng)址欄中手動(dòng)輸入path薄风,然后回車就可以看到任何頁面。這也是前端router.js寫死所有路由的弊端拍嵌。
在這里面前端只是通過后端傳回的level來給router顯示/隱藏,這樣前端維護(hù)整個(gè)路由是比較復(fù)雜的而且是有重大隱患的循诉。
現(xiàn)在呢我們來講講后端控制路由横辆。先從操作流程來說,我們這邊加入了一個(gè)dashboard中間頁茄猫,這個(gè)頁面只展示不同level下的一級(jí)路由狈蚤,通過點(diǎn)擊相應(yīng)的一級(jí)路由進(jìn)到對應(yīng)的Page頁面,該page頁面也只展示相對應(yīng)的所有的二級(jí)路由划纽。
這里面出現(xiàn)了兩個(gè)個(gè)新的概念叫 “動(dòng)態(tài)添加路由”和“導(dǎo)航守衛(wèi)”脆侮,就是我前端router.js中只寫所有人可以訪問的路由表,比如login和404頁面等勇劣。其他所有的組件資源全部寫到一個(gè)新的components.js文件中靖避,然后通過后端返回的menuData去映射符合components.js中的key,如果有對應(yīng)的比默,就把它動(dòng)態(tài)添加到router中幻捏,通過addRoutes添加。動(dòng)態(tài)添加路由這個(gè)方法要寫到導(dǎo)航守衛(wèi)beforeEach這個(gè)鉤子函數(shù)中命咐。導(dǎo)航守衛(wèi)的意思是我路由跳轉(zhuǎn)到下個(gè)頁面之前要做些什么篡九。就是說我們登錄后會(huì)跳到dashboard頁面,在進(jìn)到這個(gè)頁面之前我們需要將后端請求回來的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)入之前就做完了。這里面還有一個(gè)小的優(yōu)化的點(diǎn):當(dāng)我們通過前面說的瀏覽器菜單欄訪問到非權(quán)限頁面或者不存在的頁面時(shí)例证,需要根據(jù)vue-router中的匹配優(yōu)先級(jí)來最后addRoutes 404和*這個(gè)頁面路呜,這樣就可以直接到達(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ù)
// 一級(jí)菜單與二級(jí)菜單的區(qū)別是一級(jí)菜單帶有component這個(gè)值胀葱,比如下面的短信管理就是只有一級(jí)菜單
{
"errno": 0,
"errmsg": "獲取權(quán)限成功",
"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": "國服第一關(guān)羽",
"icon": "",
"path": "/home/addMessage",
"component": "addMessage",
"isShow": false
}
]
}
]
}
而sidebar和dashboard這兩個(gè)組件都只需要通過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){
// 只有一級(jí)菜單
if(item.component){
this.$router.push(item.path);
}else{
// 二級(jí)菜單的數(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)的整個(gè)路由數(shù)組
let routes = menuData.map((item)=>{
// 只有一級(jí)路由
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覽器控制臺(tái)改了session中的level或者在瀏覽器導(dǎo)航欄改path,都會(huì)回歸到導(dǎo)航守衛(wèi)中捅位,就是發(fā)請求重新獲取menuData轧葛,當(dāng)我addRoutes后如果沒有匹配到這個(gè)值就回到404搂抒,當(dāng)然通過改level也不會(huì)達(dá)到修改權(quán)限的控制,因?yàn)槲覀兪莿?dòng)態(tài)獲取路由尿扯,不是之前的前端控制路由求晶。
目前為止颖变,我感覺通過后端控制權(quán)限這種實(shí)現(xiàn)方式應(yīng)該是最理想的一種吧誓军,當(dāng)然大家有更好的方法或者對此文有任何問題,歡迎大家留言哈抡柿。