现在,我们应该拿到了一个获取菜单列表的接口。
我们在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即可。
所以权限管理模块本身就很简单,关键在于设计要足够灵活,动态。现在我们这样一套设计的权限管理逻辑,足够的简单,足够的强大。