需求:后台管理实现按钮级别的权限控制
解释:后台系统为公司内部人员使用,使用人的职位不同(即角色不同),则可以操作的权限不同,如超级管理员,可以定义不同角色,分配账号,分配权限菜单,高级运营可以审核信息,普通运营可以编辑文章等,不同的角色可以使用的菜单项不同,所以他们能够看到的菜单项也不一样,按钮级别的权限控制,顾名思义,就是要将权限做到按钮级别,就是不仅他们能够看到的菜单项不一样,可以使用的按钮也不一样,比如对用户的管理,涉及到“新增、编辑、删除、拉黑”,作为超级管理员,我可以操作任何一项,作为运营,我可以拉黑用户,但是作为客服,我只能看,剩下什么都做不了。(这些只是前端做的页面级的权限验证,后台也会根据token验证用户的权限的)(如下图所示)
描述:项目基于vue,使用了vuex+vue-router,模板为Element
所以上述的需求可以解释为如下:
不同的权限对应不同路由,同时侧边栏也需根据不同的权限,异步生成
思路:
后台中的拥有最大权限的角色是超级管理员,所以他能够使用的菜单和按钮一定是最全的,也就是在需求确定的前提下,组件是确定的,即全部的菜单项是确定的,通常格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29export const asyncRouterMap = [
{
path: '/course',
component: Layout,
redirect: '/course/list',
meta: {
icon: 'table',
id: 11
},
children: [
{
path: 'type',
component: _import('course/type'),
meta: { title: 'courseType', icon: 'course-type', id: 12 }
},
{
path: 'child-type',
component: _import('course/child-type'),
meta: { title: 'childType', icon: 'child-type', id: 13 }
},
{
path: 'list',
component: _import('course/index'),
meta: { title: 'courseList', icon: 'course-list', id: 14 }
}
]
}
// id 事先不可知,在后台增加了菜单项之后才知道id,所有的菜单项是固定的,所以后期如果新增菜单项,这里改动也不会太大
];登录成功后,服务端返回token,根据这个token去拉取对应的菜单和按钮列表,后台返回的数据格式如下:
1
2
3
4
5
6
7
8
9
10{
id: 11, name: "课程管理", buttonNames: null,children: [
{
id: 12, name: '课程类目' buttonNames: ['添加']
},
{
id: 13, name: '课程子类目' buttonNames: ['添加','编辑', '删除']
}
]
}数据和路由都写好了,接下来就是做匹配了。用返回的菜单和路由做匹配,返回可以操作的路由,再通过router.addRoutes动态挂载路由,添加用户可以访问的路由,这里用到了导航守卫,我的理解是,存在管理员可能会在你操作数据,使用后台的同时,无论是有意还是无意的,可能去更改你的权限,为你增加或者减少权限,所以在每一次页面跳转都会去重新获取一遍菜单列表(这里为了提升用户体验,做了30分钟的缓存),并且在匹配菜单的时候,还会根据当前的地址,查询当前页面对应的操作按钮,之所以这样做,考虑到的是匹配路由也需要遍历菜单列表,如果到每个组件mounted的时候再去遍历菜单列表得到操作按钮,就相当于多做了一次遍历操作,而且这样造成的结果就会是页面有延迟,所以就合在了一起:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 在每次页面跳珠之前,在有token的前提下生成路由
router.beforeEach((to, from, next) => {
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' });
} else {
store.dispatch('GenerateRoutes', {
isNow: false,
path: to.path
}).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters); // 动态添加可访问路由表
});
next();
}
} else {
next();
next('/login'); // 否则全部重定向到登录页
}
});使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件,根据vuex中生成的操作按钮渲染当前页面内的操作按钮:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19GenerateRoutes({ commit }, data) {
// 这里自己封装了请求缓存函数
networkRequest({
url: '/permission/ht/getMenuList',
method: 'get'
}, response => {
let accessedRouters;
console.time('路由生成时间');
accessedRouters = filterAsyncRouter(asyncRouterMap, response, data.path);
// 侧边栏路由和当前页面操作按钮分离为两个状态
commit('SET_ROUTERS', accessedRouters);
commit('SET_BTNS', buttonNames);
console.timeEnd('路由生成时间');
}, {
cacheName: 'menubar',
cacheTime: 1800000,
isNow: data.isNow
});
}匹配路由,之所以我们能够对菜单列表和路由做匹配,是因为定义路由的时候配置了meta字段(路由元信息),通过对比菜单列表的id和路由的id,返回路由信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45let buttonNames = [];
function hasPermission(menuArr, route) {
if (route.meta && route.meta.id) {
let childIndex = menuArr.findIndex(item => item.id === route.meta.id);
if (childIndex !== -1) {
// 渲染侧边栏的时候,也是通过设置meta 的title属性的,将菜单列表返回的菜单名字赋给title
route.meta.title = menuArr[childIndex].name;
}
return childIndex !== -1;
} else {
return true;
}
}
function filterAsyncRouter(asyncRouterMap, menuArr, path) {
const accessedRouters = asyncRouterMap.filter(route => {
//判断当前路由是否是用户可访问的路由
if (hasPermission(menuArr, route)) {
// 如果当前路由可以访问,再去匹配子路由
if (route.children && route.children.length) {
if (route.meta && route.meta.id) {
let childArr = [];
if (menuArr.find(item => item.id === route.meta.id)) {
let childIndex = menuArr.findIndex(item => item.id === route.meta.id);
route.meta.title = menuArr[childIndex].name;
childArr = menuArr[childIndex].children;
if (path) {
// 判断当前的路由和当前页面的一级路由是否一致,如果一致,查询二级路由下的操作按钮
let pathArr = path.split('/');
if (route.path === `/${pathArr[1]}`) {
let index = route.children.findIndex(item => item.path === pathArr[2]);
buttonNames = childArr.find(item => item.id === route.children[index].meta.id).buttonNames;
}
}
}
route.children = filterAsyncRouter(route.children, childArr);
} else {
return false;
}
}
return true;
}
return false;
});
return accessedRouters;
}生成侧边栏,使用的是Element的NavMenu组件。遍历生成的路由即可,菜单的名字使用的是meta的title。
生成按钮,用computed计算得到vuex中的按钮名称数组,实际上页面内已经写好所有的按钮和绑定函数了,这里直接用v-if进行渲染。
1
<el-button @click="handleCreate" v-if="buttonNames.includes('添加')">添加</el-button>
1
2
3
4
5computed: {
buttonNames() {
return this.$store.state.permission.btnArr;
}
}
此后台是在vue-element-admin的基础上更改的,我认为他做的就非常好,而且读懂他的代码后,再去完成自己的需求,会变得容易很多,还有详细的参考文档,我是从去年12月份开始使用这个模板的,从这项目学到了很多东西,也是这个项目给我指明了一些学习方向,勾起了我的探索兴趣,非常感谢。
从这里你可以学到的:
- 更好的理解登录权限控制
- 更好的编码习惯和思考习惯
参考文档
分享阅读清单
meta 标签
1 | <meta charset="utf-8"> |
1 | // 执行效率最高的两种数组去重的方法,arr为待去重的数组 |