現在,我們應該拿到了一個獲取菜單列表的接口。
我們在store的user模塊裏,加一個獲取菜單的方法:
GetUserMenuList({ commit }) {
return new Promise((resolve, reject) => {
getUserMenu().then(
response => {
resolve(response.data)
}
).catch(error => {
reject(error)
})
})
},
在menu模塊,我們設置一個menuList來全局存儲menu列表:
const state = {
menuList: undefined,
}
const mutations = {
SET_MENU_LIST: (state, result) => {
state.menuList = result;
},
}
const actions = {
setMenuList({ commit, state }, menuSourceList) {
commit('SET_MENU_LIST', menuSourceList)
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
首先, 用戶登錄之後,會將獲取的Token傳入setToken方法,token是存儲在cookie裏面,這裏使用的是很常用的js-cookie。
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Access-Token'
const Username = 'Username'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token, { expires: 7 })
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
然後,在每次進入路由之前,我們需要判斷是否有權限進入:
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
if (getToken()) {
next('/dashboard')
} else {
next()
}
} else if (to.path === '/401' || to.path === '/404' || to.path.includes('others')) {
next()
} else if (!getToken()) {
next('/login')
} else {
const handleAuth = (source, current) => {
if (judgeCanActivate(source, current)) {
next();
} else {
next('/401')
}
}
if (store.state.menu.menuList) {
handleAuth(store.state.menu.menuList, to.path);
} else {
store.dispatch('GetUserMenuList').then((userMenuSource) => {
store.dispatch('menu/setMenuList', userMenuSource);//初始化菜單欄
handleAuth(userMenuSource, to.path);
})
}
}
})
這裏前面幾種情況是我這裏比較常見的情況,比如沒有token,就跳轉到登錄頁面;如果輸入login路由,但是有Token就跳轉到 Dashboard頁面;如果是401,404,others頁面,就不做權限判斷。
然後是其他的情況了,都是要做判斷的,首先,我們通過store.state.menu.menuList可以知道,是否應用已經緩存了menuList全局變量,如果沒有,我們就需要調用後端接口。然後judgeCanActivate是我這邊判斷的方法,這個方法目前是一個比較粗陋的版本,需要後面再重構一下:
const judgeCanActivate = (source, current) => {
let flatMenuList = [];
for (let index = 0; index < source.length; index++) {
const menu = source[index];
if (menu.type === 1 && menu.route) {
flatMenuList.push(menu);
if (menu.children && menu.children.length) {
menu.children.forEach(
m => {
if (m.type === 1 && m.route) {
flatMenuList.push(m)
}
}
)
}
}
}
let currentRoutePath = current.split('/');
const activeMenu = flatMenuList.findIndex(
(fm) => {
return fm.route === currentRoutePath[currentRoutePath.length - 1];
}
)
return activeMenu >= 0;
};
這裏的邏輯也很簡單,就是通過判斷menu數組裏的route字段是否包含當前路由最後的單詞(因爲路由有父子)。
這樣,不包含的就認爲沒有權限,會跳轉到401頁面。
我們使用store.dispatch('menu/setMenuList', userMenuSource);來設置來全局數據menuList,然後在渲染菜單欄的頁面,我們可以獲取到它:
computed: {
...mapState({
menuList: state => state.menu.menuList,
taskSearchName: state => state.app.taskSearchName
}),
},
然後我們就開始動態的渲染菜單欄。
新建一個MenuBarItem的vue文件:
<template>
<div v-if="item.type===1&&item.visible">
<template v-if="hasNoChildMenu(item.children,item)">
<el-menu-item :index="`${basePath}/${item.route}`">
{{$t(`menu.${item.route}`)}}
</el-menu-item>
</template>
<el-submenu v-else :index="(item.route)" :popper-append-to-body="false" :ref="item.route">
<template slot="title">
{{$t(`menu.${item.route}`)}}
</template>
<template v-for="child in item.children">
<menu-bar-item v-if="child.children&&child.children.length" :item="child" :is-nest="true" :key="child.route"
:base-path="`/${item.route}`" />
<el-menu-item v-else :index="`/${item.route}/${child.route}`" :key="child.route">
{{$t(`menu.${child.route}`)}}
</el-menu-item>
</template>
</el-submenu>
</div>
</template>
<script>
export default {
name: "MenuBarItem",
props: {
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ""
}
},
methods: {
hasNoChildMenu(children, parent) {
if (!children) {
return true;
}
const showingChildren = children.filter(item => {
return item.visible;
});
if (showingChildren.length >= 1) {
return false;
} else {
return true;
}
}
}
};
</script>
basePath就是父級路由的地址。
代碼看上去很簡潔。hasNoChildMenu方法來判斷是否有子菜單,如果有,會渲染<el-submenu>元素,注意這裏有一個遞歸的算法,如果子元素還有子元素,會渲染自身的menu-bar-item元素,否則就是el-menu-item元素。
這樣子不管下面有多少層children,都可以高效的渲染出來。
然後在最外面使用整個組件:
<menu-bar-item v-for="menu in menuList" :key="menu.path" :item="menu" :base-path="''" />
這裏會遇到一個BUG,因爲我們採用組件渲染菜單欄,那麼el-menu-item外面多了一層div,導致Element默認的菜單樣式不生效了,解決方法也很簡單,就是把樣式自定義,但是加上div,比如:
.el-menu--horizontal>div>.el-menu-item,
.el-menu--horizontal>div>.el-submenu .el-submenu__title {
height: 64px !important;
line-height: 64px !important;
&:hover {
background-color: rgba(255, 255, 255, .2) !important;
}
}
但是水平方向菜單,每個menu-item一直佔用整個屏幕寬度,將其設置爲flex佈局:
.el-menu--horizontal {
display: flex;
}
現在,菜單欄就會根據接口動態的渲染,輸入地址也會根據權限判斷是否可以進入。而且我們還拿到了每個頁面的具體按鈕功能的權限,至於在頁面裏面如何隱藏按鈕,那都不是事兒。
你也可以把前端這一套搬到Angular裏,只要把router.beforeEach方法改爲canActivate()裏面,store的數據管理改爲rxJS即可。
所以權限管理模塊本身就很簡單,關鍵在於設計要足夠靈活,動態。現在我們這樣一套設計的權限管理邏輯,足夠的簡單,足夠的強大。