如何在 vue 项目中构建权限菜单

在使用 vue 做中后台管理系统项目时,我们经常会涉及到权限管理,即对菜单树进行权限控制,由于不同框架本身的一些特性,通常实现权限功能的方式也大不相同。

本文就 vue 项目来介绍一种菜单树的权限管理方式。

前言

对于 vue 项目来说,我们通常会将 菜单树路由表 进行关联,也就是说,最终的菜单树 会根据 完整的路由表 生成。

这里 完整的路由表 可以分为:静态路由动态路由 这两部分。

  • 静态路由

指的是,用户可正常访问的路由,诸如 /login/404/about 等。这些路由信息不会与菜单树关联。

  • 动态路由

指的是,有对应权限的用户才可以访问,这些路由信息会与菜单树进行关联。

那么,最终的菜单树 又可以如何确定呢?

我们可以借鉴像 vue-element-admin 这样的后台模板框架实现菜单树的思路,对 完整的路由表 中路由对象 hidden 属性为 false 的项作为一个菜单树节点。

1
2
3
4
5
6
7
8
9
[
{
name: 'system',
path: '/system',
//当设置 true 的时候该路由不会在侧边栏出现 如 401,login等页面,或者如一些编辑页面 /edit/1
hidden: false,
component: () => import('@/views/system')
}
]

资源管理

为了做到对页面菜单项的灵活控制,我们的项目中应该有一个叫做 资源管理 的页面。该页面主要的作用是可以很方便的 新增、修改、删除 一个权限菜单项。

而该资源菜单树的每一个菜单项可以使用一个唯一的 权限编码(code) 与动态路由对象进行关联。

也就是说,后台存储的是一个有 权限编码(code) 信息的菜单树形数据结构,如下所示:

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

const authTree = [
{
code: "system_manage",
name: "系统管理",
child: [
{
code: "system_user_menu",
name: "用户管理"
},
{
code: "system_role_menu",
name: "角色管理"
},
{
code: "system_function_menu",
name: "资源管理"
}
]
},
{
code: "product_manage",
name: "商品管理",
child: [
{
code: "product_add_menu",
name: "添加商品"
},
{
code: "product_all_menu",
name: "所有商品"
},
{
code: "product_attribute_menu",
name: "属性管理"
}
]
}
];

如上数据结构所示,code 属性表示该菜单节点的权限编码,name 属性表示该菜单节点的名称,当然,你也可以加入一些其他的可控字段。

动态路由定义

我们已经维护好了一份资源菜单树,那么,接下来的工作就是对每个 权限编码 去定义对应的路由对象信息,这样,我们就可以在后面通过 遍历权限菜单树 去生成一份动态路由表出来。

前端定义的 动态路由 类似于下面这样:

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
45
46
47
48
49
50
const dynamicRoutes = {
system_manage: {
name: "systemmanage",
path: "/systemmanage",
component: "this is a component load with import() method",
meta: { title: "系统管理", icon: "iconfont icon-xitongguanli" }
},
system_user_menu: {
name: "usermanage",
path: "/usermanage",
component: "this is a component load with import() method",
meta: { title: "用户管理", icon: "iconfont icon-xitongguanli" }
},
system_role_menu: {
name: "rolemanage",
path: "/rolemanage",
component: "this is a component load with import() method",
meta: { title: "角色管理", icon: "iconfont icon-xitongguanli" }
},
system_function_menu: {
name: "sourcemanage",
path: "/sourcemanage",
component: "this is a component load with import() method",
meta: { title: "资源管理", icon: "iconfont icon-xitongguanli" }
},
product_manage: {
name: "productmanage",
path: "/productmanage",
component: "this is a component load with import() method",
meta: { title: "商品管理", icon: "iconfont icon-xitongguanli" }
},
product_add_menu: {
name: "addproduct",
path: "/addproduct",
component: "this is a component load with import() method",
meta: { title: "添加商品", icon: "iconfont icon-xitongguanli" }
},
product_all_menu: {
name: "allproduct",
path: "/allproduct",
component: "this is a component load with import() method",
meta: { title: "所有商品", icon: "iconfont icon-xitongguanli" }
},
product_attribute_menu: {
name: "attributemanage",
path: "/attributemanage",
component: "this is a component load with import() method",
meta: { title: "属性管理", icon: "iconfont icon-xitongguanli" }
}
};

根据权限树生成路由表

权限树数据 和 动态路由定义已经处理好了,现在,我们就要根据这两份数据生成 vue-router 可用的路由表数据了。

可以使用如下函数进行生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 根据权限树生成可用的路由表
function filterMenu(res, authTree, routes) {
authTree.forEach(v => {
const routeObj = routes[v.code],
name = v.name,
child = v.child;

if ( name ) routeObj.meta.title = name;

if (child && child.length > 0) routeObj.children = filterMenu([], child, routes);

res.push(routeObj);
});

return res;
}

console.log(filterMenu([], authTree, dynamicRoutes));

插入到路由表中

最后,我们可以使用 addRoutes 方法将生成的路由表数据添加到路由中,这样,路由、权限和菜单就进行了很好的关联,后续对菜单项的控制也会非常方便。

1
2
3
4
5
6
7
8
9
// ...
import router from '@/router'

// ...

router.addRoutes( routes )

// ...

上面的 routes 就是通过比对权限后生成的路由表数据了。

用户角色管理

通常,我们的权限是绑定到角色的,不同的角色有不同的权限,所以,我们可以在 用户角色管理 模块去绑定对应的菜单权限。

页面如何设计就看你自己了。

动态路由与后台关联的另一种方式

在第一种方式中,当将菜单权限赋予某个角色后,那么,该角色对于的菜单树结构就已经成型了,只不过需要将菜单 权限code 替换为真正的路由对象。再在适合的时机插入到路由表中。

这里还有另一种实现方式。我们将完整的动态路由同步到后台数据库,除了路由对象中的 component 字段变为 String 类型以外,该动态路由没有任务其他特殊之处。

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
[
{
name: 'system',
path: '/system',
component: 'Layout',
meta: {
title: '系统管理',
icon: 'system-icon',
roles: ['admin']
},
children: [
{
name: 'rolesManagement',
path: 'roles_management',
component: 'rolesManagement',
meta: {
title: '角色管理',
icon: 'roles-icon',
roles: ['admin']
}
}
// ...
]
}
]

如上动态路由所示,我们可以看到两个比较特殊的地方,第一个是 component 字段,第二个是 meta 中的 roles 字段。他们有什么意义呢?

component字段

该字段表示路由对象所映射的组件。由于我们需要将整个动态路由表存储到后台数据库,所以,原先的 component 设置方式不再适用,这里我们该用字典关联的方式来解决。我们会有这样一个文件,存储着动态路由中的所有动态组件。

1
2
3
4
5
6
// dynamicRoutesMap.js
export default {
Layout: () => import('@/layout'),
rolesManagement: () => import('@/views/system/rolesManagement'),
// ...
}

roles字段

该字段表示路由对象所关联的角色有哪些。也就是说,只要某个路由对象中的 roles 信息中的角色是当前用户所拥有的,那么该路由对象就该被显示出来。当然,roles 字段是后台动态追加进去的,可在 用户角色管理 页面进行处理。

如何过滤出当前用户有权访问的路由表

获取当前用户信息

我们首先需要获取当前用户信息,即,当前用户拥有的角色信息。

1
2
3
4
5
{
name: '张三',
id: 1,
roles: ['role1', 'role2', 'role3']
}

如上所示,张三 同时拥有 role1role2role3 这三个角色。

获取动态路由信息

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
45
[
{
name: 'system',
path: '/system',
component: 'Layout',
meta: {
title: '系统管理',
icon: 'system-icon',
roles: ['role1', 'admin']
},
children: [
{
name: 'rolesManagement',
path: 'roles_management',
component: 'rolesManagement',
meta: {
title: '角色管理',
icon: 'roles-icon',
roles: ['role1', 'admin'']
}
},
{
name: 'usersManagement',
path: 'users_management',
component: 'usersManagement',
meta: {
title: '用户管理',
icon: 'users-icon',
roles: ['role1', 'admin']
}
},
{
name: 'resourceManagement',
path: 'resource_management',
component: 'resourceManagement',
meta: {
title: '资源管理',
icon: 'resource-icon',
roles: ['admin']
}
},
// ...
]
}
]

如上所示,这一个经过后台处理的含有完整 roles 信息的动态路由表。每个路由对象都有对应的 roles 信息。表示该路由对象对哪些角色可见。

经过过滤处理后,实际生成的动态路由表示这样的。

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
[
{
name: 'system',
path: '/system',
component: 'Layout',
meta: {
title: '系统管理',
icon: 'system-icon',
roles: ['role1', 'admin']
},
children: [
{
name: 'rolesManagement',
path: 'roles_management',
component: 'rolesManagement',
meta: {
title: '角色管理',
icon: 'roles-icon',
roles: ['role1', 'admin'']
}
},
{
name: 'usersManagement',
path: 'users_management',
component: 'usersManagement',
meta: {
title: '用户管理',
icon: 'users-icon',
roles: ['role1', 'admin']
}
},
// ...
]
}
]

因为,当前用户没有 admin 角色权限,所以 资源管理 这个路由对象被剔除。

最后,我们依然会使用 addRoutes 方法将生成的 动态路由表 插入到路由表中,菜单树,也会根据动态路由渲染出来。