背景

最近又开始了一个新的项目,最近一直都在做项目的前期准备工作,借着这样的机会,我们来唠嗑唠嗑前端项目从0到1过程。今天我就先来说说前端动态路由和鉴权这块。大神勿喷。


前言

第一次看到动态路由是在研究vue-element-admin代码的时候。之前我接触的系统都是后台返回给前端一个路由表,然后前端直接渲染在页面上就可以了,但是看到了大佬的代码后,感觉这种思路很好,毕竟现在前后端的分离了,路由和权限这些信息应该由前端来处理。所以我第一次尝试了前端动态鉴权。虽然逻辑有点复杂,但是最终我还是实现了。

实现思路

  1. 每次路由跳转都判断用户是否登录,登录了才会进行后续操作,否则直接跳到登录页面
  2. 浏览器中已经保存了用户登录信息,但是请求进入的页面是登录页面,直接进入到首页,实现免登录的功能
  3. 判断vuex中是否存在路由配置信息,如果存在直接放行,否则进行下一步操作。
  4. 如果vuex中不存在路由配置信息,就需要去后台请求当前用户对应角色的权限信息,根据权限信息生成可访问的路由表,并存储在vuex中。
  5. 合并路由,将初始路由和生成的动态路由进行拼接。

实现

  1. 在router.js中定义两份路由,一份是初始路由,另一份是系统功能路由,需要动态加载
    基础路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 基础路由
*/
export const constantRouterMap = [
{
path: '/login',
name: 'login',
hidden: true,
component: () => import('@/pages/login/Login')
},
{
path: '/404',
component: () => import('@/pages/exception/404')
}
];

系统功能路由(截取部分):

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* 系统功能路由,动态加载
* 需要根据实际情况自己配置
*/
export const ansycRouterMap = [
{
path: '/',
name: 'index',
component: MenuView,
meta: { title: '首页' },
redirect: '/service/servicePage',
children: [
{
path: '/service/servicePage',
name: 'servicePage',
component: () => import('@/pages/service/ServicePage'),
meta: {
title: '首页',
icon: 'dashboard',
keepAlive: true,
permission: ['SERVICE_CATALOG']
}
},
// 变更
{
path: '/change',
name: 'change',
component: RouterView,
redirect: '/change/changeList',
meta: {
title: '变更管理',
icon: 'credit-card',
permission: ['MOD_MANAGE']
},
children: [
{
path: '/change/changeList',
name: 'changeList',
component: () => import('@/pages/modify/ModifyList'),
meta: {
title: '变更列表',
keepAlive: true,
permission: ['MOD_LIST']
}
},
{
path: '/change/addChange',
name: 'addChange',
hidden: true,
component: () => import('@/pages/modify/CreateChange'),
meta: { title: '新增变更', permission: ['MOD_LIST'] }
},
{
path: '/change/changeInfo',
name: 'changeInfo',
hidden: true,
component: () => import('@/pages/modify/ChangeInfo'),
meta: { title: '变更详情', permission: ['MOD_LIST'] }
}
]
},
]
},
{
path: '*',
redirect: '/service/servicePage',
hidden: true
}
];
  1. permission.js
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 路由守卫
router.beforeEach((to, from, next) => {
// 开启页面上方进度条
NProgress.start();
// 判断用户是否登录
// 用户登录成功后台会返回一个token,这里我将token保存在localStorage中,
// 并设置一个过期时间,和后台过期时间保持一致
if (Vue.ls.get(ACCESS_TOKEN)) {
// 浏览器中已经有用户登录信息,并且用户要去登录页面
// 直接放行,跳转到系统首页,实现免登录
if (to.path === '/login') {
next({ path: '/' });
// 关闭页面上方进度条
NProgress.done();
} else {
// 判断vuex中是否有登录信息
if (Store.getters.addRouters === null) {
// 后台请求用户信息
// 特地封装的一个接口,包括用户的基本信息和角色权限信息
Store.dispatch('getUserInfo')
.then(res => {
if (res.code === '111111') {
const roles = res.object;
if (roles.userPrivilegeCodes && roles.userPrivilegeCodes.length > 0) {
Store.dispatch('GenerateRoutes', { roles }).then(() => {
// 根据roles权限生成可访问的路由表
// 动态添加可访问路由表
router.addRoutes(Store.getters.addRouters);
const redirect = decodeURIComponent(from.query.redirect || to.path);
if (redirect === to.path) {
// hack方法 确保addRoutes已完成,
// set the replace: true so the navigation will not leave a history record
next({ ...to, replace: true });
} else {
next({ path: redirect });
}
});
} else {
// 用户没有对应的角色权限信息
notification.error({
message: '错误',
description: '当前用户无权限菜单,请联系系统管理员'
});
Store.dispatch('LogOut').then(() => {
next({ path: '/login', query: { redirect: to.fullPath } });
NProgress.done();
});
}
} else {
// 获取用户信息失败
notification.error({
message: '错误',
description: '获取用户信息失败,请重试'
});
Store.dispatch('LogOut').then(() => {
next({ path: '/login', query: { redirect: to.fullPath } });
NProgress.done();
});
}
})
.catch(() => {
notification.error({
message: '错误',
description: '获取用户信息失败,请重试'
});
Store.dispatch('LogOut').then(() => {
next({ path: '/login', query: { redirect: to.fullPath } });
NProgress.done();
});
});
} else {
next();
}
}
} else {
// 浏览器中没有cookie信息
// 判断跳转路由是否免登录
// 免登录直接放行
// 否则返回登录页面(也可跳到提示页面)
if (whiteList.includes(to.name)) {
next();
} else {
next({ path: '/login', query: { redirect: to.fullPath } });
NProgress.done();
}
}
});

// 路由跳转完成之后的回调
router.afterEach(() => {
// 关闭加载条
NProgress.done();
});

以上代码是动态路由实现的核心代码,主要是使用了vue-router提供的两个钩子函数,代码逻辑判断比较多,极易出现路由循环跳转从而引起爆栈问题。所以在写的时候一定要注意。
下面我们再来看看vuex中核心的构造路由表的代码

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
51
52
53
54
55
56
57
58
import {
constantRouterMap,
ansycRouterMap
} from '@/router/config/router.config';

function hasPermission(permission, route) {
if (route.meta && route.meta.permission) {
let flag = false;
for (let i = 0, len = permission.length; i < len; i++) {
flag = route.meta.permission.includes(permission[i]);
if (flag) {
return true;
}
}
return false;
}
return true;
}

function filterAsyncRouter(routerMap, roles) {
return routerMap.filter(route => {
if (hasPermission(roles.userPrivilegeCodes, route)) {
if (route.children && route.children.length) {
// 递归调用,确保子元素能够被加载到
route.children = filterAsyncRouter(route.children, roles);
}
return true;
}
return false;
});
}

const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise((resolve, reject) => {
const { roles } = data;
const accessedRouters = filterAsyncRouter(ansycRouterMap, roles);
commit('SET_ROUTERS', accessedRouters);
resolve();
});
}
},
mutations: {
SET_ROUTERS: (state, routers) => {
// 侧边栏路由
state.addRouters = routers;
// 路由表
state.routers = constantRouterMap.concat(routers);
}
}
};

export default permission;

以上就是动态路由的核心代码,当然如果你看到这儿还是一脸懵逼,你可以去我的github看看,哪里我已经准备好了一个完整demo vue-config-web,欢迎star。


本文完