动态路由+路由鉴权

动态路由 + 路由鉴权

路由鉴权介绍

一般 Vue 的权限控制有两种方案:

  • 路由元信息(meta)
  • 动态加载菜单和路由(addRoutes)

路由元信息(meta)

如果一个网站有不同的角色,比如管理员普通用户,要求不同的角色能访问的页面是不一样的

这个时候我们就可以把所有的页面都放在路由表里,只要在访问的时候判断一下角色权限。如果有权限就让访问,没有权限的话就拒绝访问,跳转到 404 页面。

vue-router 在构建路由时提供了元信息 meta 配置接口,我们可以在元信息中添加路由对应的权限,然后在路由守卫中检查相关权限,控制其路由跳转。

可以在每一个路由的 meta 属性里,将能访问该路由的角色添加到 roles 里。用户每次登陆后,将用户的角色返回。然后在访问页面时,把路由的 meta 属性和用户的角色进行对比,如果用户的角色在路由的 roles 里,那就是能访问,如果不在就拒绝访问。

代码示例 1:

路由信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
routes: [
{
path: '/login',
name: 'login',
meta: {
roles: ['admin', 'user']
},
component: () => import('../components/Login.vue')
},
{
path: 'home',
name: 'home',
meta: {
roles: ['admin']
},
component: () => import('../views/Home.vue')
}
];

页面控制:

1
2
3
4
5
6
7
8
9
10
11
//假设有两种角色:admin 和 user
//从后台获取的用户角色
const role = 'user'
//当进入一个页面是会触发导航守卫 router.beforeEach 事件
router.beforeEach((to,from,next)=>{
if(to.meta.roles.includes(role)){
next() //放行
}esle{
next({path:"/404"}) //跳到404页面
}
})

代码示例 2

当然也可以用下面的一种方法:

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
// router.js
// 路由表元信息
[
{
path: '',
redirect: '/home'
},
{
path: '/home',
meta: {
title: 'Home',
icon: 'home'
}
},
{
path: '/userCenter',
meta: {
title: '个人中心',
requireAuth: true // 在需要登录的路由的meta中添加响应的权限标识
}
}
];

// 在守卫中访问元信息
function gaurd(to, from, next) {
// to.matched.some(record => record.meta.requireAuth)
// 可在此处
}

可以在多个路由下面添加这个权限标识,达到控制的目的

只要一切换页面,就需要看有没有这个权限,所以可以在最大的路由下 main.js 中配置

存储信息

一般的,用户登录后会在本地存储用户的认证信息,可以用 tokencookie 等,这里我们用 token

将用户的token保存到localStorage里,而用户信息则存在内存store中。这样可以在vuex中存储一个标记用户登录状态的属性 auth,方便权限控制。

代码示例

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
// store.js
{
state: {
token: window.localStorage.getItem('token'),
auth: false,
userInfo: {}
},
mutations: {
setToken (state, token) {
state.token = token
window.localStorage.setItem('token', token)
},
clearToken (state) {
state.token = ''
window.localStorage.setItem('token', '')
},
setUserInfo (state, userInfo) {
state.userInfo = userInfo
state.auth = true // 获取到用户信息的同时将auth标记为true,当然也可以直接判断userInfo
}
},
actions: {
async getUserInfo (ctx, token) {
return fetchUserInfo(token).then(response => {
if (response.code === 200) {
ctx.commit('setUserInfo', response.data)
}
return response
})
},
async login (ctx, account) {
return login(account).then(response => {
if (response.code === 200) {
ctx.commit('setUserInfo', response.data.userInfo)
ctx.commit('setToken', response.data.token)
}
})
}
}
}

写好路由表和 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
// router.js
router.beforeEach(async (to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)) {
// 检查是否需要登录权限
if (!store.state.auth) {
// 检查是否已登录
if (store.state.token) {
// 未登录,但是有token,获取用户信息
try {
const data = await store.dispatch('getUserInfo', store.state.token);
if (data.code === 200) {
next();
} else {
window.alert('请登录');
store.commit('clearToken');
next({ name: 'Login' });
}
} catch (err) {
window.alert('请登录');
store.commit('clearToken');
next({ name: 'Login' });
}
} else {
window.alert('请登录');
next({ name: 'Login' });
}
} else {
next();
}
} else {
next();
}
});

上述的方法是基于jwt认证方式,本地不持久化用户信息,只保存token,当用户刷新或者重新打开网页时,进入需要登录的页面都会尝试去请求用户信息,该操作在整个访问过程中只进行一次,直到刷新或者重新打开,对于应用后期的开发维护和扩展支持都很好。

动态加载菜单和路由(addRoutes)

有时候为了安全,我们需要根据用户权限或者是用户属性去动态的添加菜单和路由表,可以实现对用户的功能进行定制。vue-router提供了addRoutes()方法,可以动态注册路由,需要注意的是,动态添加路由是在路由表中 push 路由,由于路由是按顺序匹配的,因此需要将诸如 404 页面这样的路由放在动态添加的最后。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// store.js
// 将需要动态注册的路由提取到vuex中
const dynamicRoutes = [
{
path: '/manage',
name: 'Manage',
meta: {
requireAuth: true
},
component: () => import('./views/Manage')
},
{
path: '/userCenter',
name: 'UserCenter',
meta: {
requireAuth: true
},
component: () => import('./views/UserCenter')
}
];

vuex中添加userRoutes数组用于存储用户的定制菜单。在 setUserInfo 中根据后端返回的菜单生成用户的路由表。

1
2
3
4
5
6
7
8
9
10
// store.js
setUserInfo (state, userInfo) {
state.userInfo = userInfo
state.auth = true // 获取到用户信息的同时将auth标记为true,当然也可以直接判断userInfo
// 生成用户路由表
state.userRoutes = dynamicRoutes.filter(route => {
return userInfo.menus.some(menu => menu.name === route.name)
})
router.addRoutes(state.userRoutes) // 注册路由
}

修改菜单渲染

1
2
3
4
5
6
7
8
9
10
11
12
// App.vue
<div id="nav">
<router-link to="/">主页</router-link>
|
<router-link to="/login">登录</router-link>
<template v-for="(menu, index) of $store.state.userInfo.menus">
|
<router-link :to="{ name: menu.name }" :key="index">
{{menu.title}}
</router-link>
</template>
</div>

前面铺垫了这么多,终于到本篇博客的主题了,下面我们要自己实现一套「动态生成无限菜单+路由」的方案,也就是第二种路由鉴权。

最终实现的菜单列表

1
2
3
4
5
6
个人中心  Profile.vue
商店 Shop.vue
购物车 Cart.vue
└── 购物车列表 CartList.vue
├── 商品 Product.vue
└── 彩票 Lottery.vue

基本思路

  1. 后端返回路由权限列表(带权限标识,假定为 auth)
  2. 前端进行全局的路由拦截,判断自身有没有获取过后端返回的路由列表
  3. 如果没有则获取数据并保存到 vuex 中,动态的生成菜单和路由,然后进行 next 跳转
  4. 如果有则不再获取,直接 next 跳转

其中第三步是核心点,它还可以细分为下列步骤:

  1. 菜单栏往往有多级嵌套,所以得根据后端返回的数据进行转换,最终生成无限级菜单;并在转换过程中找到所有可以有权访问的路由名称清单(根据上文提到的 auth 标识寻找)
  2. 前端分别保存两种路由列表,第一种是不需要权限就能访问的,例如首页;第二种是需要等级权限才能访问的路由列表,例如新增 admin 管理员
  3. 动态生成路由列表(在第二种路由列表中,通过路由名称清单过滤)

后台返回的 admin 路由权限

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
[
{ pid: -1, path: '/cart', name: '购物车', id: 1, auth: 'cart' },
{
pid: 1,
path: '/cart/cart-list',
name: '购物车列表',
id: 4,
auth: 'cart-list'
},
{
pid: 4,
path: '/cart/cart-list/lottery',
auth: 'lottery',
id: 5,
name: '彩票'
},
{
pid: 4,
path: '/cart/cart-list/product',
auth: 'product',
id: 6,
name: '商品'
},
{ pid: -1, path: '/shop', name: '商店', id: 2, auth: 'shop' },
{ pid: -1, path: '/profile', name: '个人中心', id: 3, auth: 'store' }
];

项目生成

1
2
3
4
5
6
7
8
vue create menu-auth
? Check the features needed for your project:
◉ Babel
◉ Router
◉ Vuex
history mode
In dedicated config files
Save this as a preset for future projects? (y/N) n

移除不必要的文件及其代码

  • components/HelloWorld.vue
  • views/About.vue
  • views/Home.vue 中 .home 标签下所有内容,有关 HelloWorld.vue 相关代码
  • App.vue 中 #app 下除 router-view 所有内容
  • router.js 下有关 About.vue 相关代码

后端生成

根目录下创建 server.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
let express = require('express');
let app = express();
// 在后端配置,让所有人都可以访问api接口 跨域问题
app.use('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
// Access-Control-Allow-Headers ,可根据浏览器的F12查看,把对应的粘贴在这里就行
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});
// 前端请求的路由API路径
app.get('/role', (req, res) => {
res.json({
//假如这是后端给我们的JSON数据
menuList: [
{
pid: -1,
path: '/cart',
name: '购物车',
id: 1,
auth: 'cart'
},
{
pid: 1,
path: '/cart/cart-list',
name: '购物车列表',
id: 4,
auth: 'cart-list'
},
{
pid: 4,
path: '/cart/cart-list/lottery',
auth: 'lottery',
id: 5,
name: '彩票'
},
{
pid: 4,
path: '/cart/cart-list/product',
auth: 'product',
id: 6,
name: '商品'
},
{
pid: -1,
path: '/shop',
name: '商店',
id: 2,
auth: 'shop'
},
{
pid: -1,
path: '/profile',
name: '个人中心',
id: 3,
auth: 'profile'
}
],
buttonAuth: {
edit: true // 可编辑
}
});
});
//监听3000端口
app.listen(3000);

终端 node server.js 启动,或者使用 nodemon server.js 热启动。

新建路由所需相关文件

components/menu/ 文件下新建:

cart-list.vue

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
cart-list
<router-view></router-view>
</div>
</template>

<script>
export default {};
</script>

<style></style>

cart.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
cart
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'cart'
};
</script>

<style></style>

lottery.vue

1
2
3
4
5
6
7
8
9
<template>
<div>lottery</div>
</template>

<script>
export default {};
</script>

<style></style>

product.vue

1
2
3
4
5
6
7
8
9
<template>
<div>product</div>
</template>

<script>
export default {};
</script>

<style></style>

profile.vue

1
2
3
4
5
6
7
8
9
<template>
<div>profile</div>
</template>

<script>
export default {};
</script>

<style></style>

shop.vue

1
2
3
4
5
6
7
8
9
<template>
<div>shop</div>
</template>

<script>
export default {};
</script>

<style></style>

最后来一个 404 页面,放在 views/ 文件夹下:

1
2
3
4
5
6
7
8
9
<template>
<div>404</div>
</template>

<script>
export default {};
</script>

<style></style>

配置路由

来到 router.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
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';

Vue.use(Router);

export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
//访问'/'时,重定向到home页面
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home
},
{
path: '/cart',
name: 'cart',
//使用懒加载,当使用这个组件的时候再加载资源,当组件资源较大时,不建议使用,可能会出现白屏现象
//而且最好使用绝对路径,@是绝对路径的意思,相当于src下
component: () => import('@/components/menu/cart.vue'),
//配置子路由
children: [
{
//当配置子路由时,最好不要在前面加'/',比如:'/cart-list'
path: 'cart-list',
name: 'cart-list',
component: () => import('@/components/menu/cart-list.vue'),
//配置子路由
children: [
{
path: 'lottery',
name: 'lottery',
component: () => import('@/components/menu/lottery.vue')
},
{
path: 'product',
name: 'product',
component: () => import('@/components/menu/product.vue')
}
]
}
]
},
{
path: '/profile',
name: 'profile',
component: () => import('@/components/menu/profile.vue')
},
{
path: '/shop',
name: 'shop',
component: () => import('@/components/menu/shop.vue')
},
{
path: '*',
component: () => import('@/views/404.vue')
}
]
});

配置完成后 npm run serve 跑一下项目,分别进入几个路由路径,看是否能正确打开相应页面。

获取后端 admin 权限数据

为了拿到后端返回的数据,我们需要利用 axios 发起异步请求,并把响应回来的数据存放到 vuex 中去,并且还需要对后端数据做一个转换,将其转换为下面格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 后端格式
[
{id: 1, name: 'a', pid: -1},
{id: 2, name: 'b', pid: -1},
{id: 3, name: 'c', pid: 1},
{id: 4, name: 'd', pid: 1}
]
↓↓↓
[
{id: 1, name: 'a', pid: -1, children: [
{id: 3, name: 'c', pid: 1, children: null},
{id: 4, name: 'd', pid: 1, children: null}
]},
{id: 2, name: 'b', pid: -1, children: null}
]

安装 axios

1
npm install axios

在全局路由守卫中监听是否已获取过路由权限数据

路由权限的数据,为了安全我们需要页面切换时就再次获取,所以可以用全局路由守卫帮助我们实现,逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 只要页面切换就执行的钩子
router.beforeEach(async (to, from, next) => {
// 判断当前有没有获取过权限,如果获取过了,就不要再获取了
if (!store.state.hasRules) {
//获取权限,调用获取权限的接口,去action中获取数据
await store.dispatch('getMenuList');
next();
} else {
//如果已经获取了权限就可以访问页面了
next();
}
});

编写 vuex + 数据转化(递归)

根据上面钩子函数中的逻辑,在 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
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

// 后端数据转化
let formatMenuList = menuList => {
function r(pid) {
//filter过滤数组,返回一个满足要求的数组
return menuList.filter(menu => {
//格式化菜单变成我们需要的结果
if (menu.pid === pid) {
let children = r(menu.id);
menu.children = children.length ? children : null;
return true;
}
});
}
return r(-1);
};

export default new Vuex.Store({
state: {
// 存放菜单权限数据
menuList: [],
// 原本只要判断menuList.length是否不为空就能判定是否已经获取过menuList,
// 但如果后台返回空数组咋办?所以此处还需要一个hasRules变量来标识是否已获取
// 做法就是,获取完毕后,把hasRules改为true
hasRules: false
},
mutations: {},
actions: {
async getMenuList() {
let { data } = await axios.get('http://localhost:3000/role');
let menuList = data.menuList;
menuList = formatMenuList(menuList);
console.log(menuList);
}
}
});

但目前有个小问题,即我们在 router.js 中配置了所有路由,然后根据后端返回的权限来动态展示相应菜单,从而实现跳转。但如果用户猜出了不允许他访问的路由,直接修改 url 跳转怎么办?那他不是也能访问了吗?

所以我们需要让后端在返回的数据中,给每个路由对象添加一个 最开始提到的 auth 标识:

1
{ pid: -1, path: '/cart', name: '购物车', id: 1, auth: 'cart' }

如果有 auth 标识,代表当前用户能够访问,如果没有,则表示当前用户不能访问,这样我们前端就能据此动态的删除掉不能访问的路由。

回到 src/store.js ,我们需要把后端这个 auth 属性都保存起来。首先在 state 中加上 authList 属性,用来存储哪些路由能有权限访问:

1
2
3
state: {
authList: [];
}

然后在 formatMenuList 方法中,提取权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let formatMenuList = menuList => {
let arr = []; // [cart, cart-list, profile, ...]
function r(pid) {
//filter过滤数组,返回一个满足要求的数组
return menuList.filter(menu => {
//格式化菜单变成我们需要的结果
if (menu.pid === pid) {
arr.push(menu.auth); // 把后端返回的所有路径权限都放到数组中
let children = r(menu.id);
menu.children = children.length ? children : null;
return true;
}
});
}
return { menuL: r(-1), authL: arr };
};

这样改造后,formatMenuList 方法就返回一个对象了,该对象中包含路由菜单和路由权限两个数组。

相应的,我们还得改造使用了 formatMenuList 方法的 action ——> getMenuList

1
2
3
4
5
6
7
async getMenuList({ commit }) {
let { data } =await axios.get('http://localhost:3000/role')
let { menuL, authL } = formatMenuList(data.menuList)
console.log(menuL, authL)
commit('set_menuList', menuL)
commit('set_authList', authL)
}

对应的,编写两个 mutation :

1
2
3
4
5
6
7
set_menuList(state, m) {
state.menuList = m
},
set_authList(state, a) {
state.authList = a
state.hasRules = true
}

保存后 console.log 一下,menuList 和 authList 就出来了。

渲染无限菜单

配置 element-ui

安装:

1
npm install element-ui

main.js 中进行配置:

1
2
3
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

在 Home.vue 页面中引入静态菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="home">
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-submenu index="1">
<template slot="title">导航一</template>
<el-submenu index="1-1">
<template slot="title">选项1-1</template>
<el-menu-item index="1-1-1">选项1-1-1</el-menu-item>
<el-menu-item index="1-1-2">选项1-1-2</el-menu-item>
</el-submenu>
<el-menu-item index="1-2">选项1-2</el-menu-item>
</el-submenu>
<el-menu-item index="2">导航二</el-menu-item>
<el-menu-item index="3">导航三</el-menu-item>
<el-menu-item index="4">导航四</el-menu-item>
</el-menu>
</div>
</template>

刷新页面后就能显示静态的 Element 菜单了。但点击某个菜单选项后浏览器无法跳转,所以我们还得给 <el-menu></el-menu> 标签设置个 router 属性为 true ,这样就能根据菜单的 index 属性进行路由跳转了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="home">
<el-menu default-active="2" class="el-menu-vertical-demo" :router="true">
<el-submenu index="1">
<template slot="title">导航一</template>
<el-submenu index="1-1">
<template slot="title">选项1-1</template>
<el-menu-item index="1-1-1">选项1-1-1</el-menu-item>
<el-menu-item index="1-1-2">选项1-1-2</el-menu-item>
</el-submenu>
<el-menu-item index="1-2">选项1-2</el-menu-item>
</el-submenu>
<el-menu-item index="2">导航二</el-menu-item>
<el-menu-item index="3">导航三</el-menu-item>
<el-menu-item index="4">导航四</el-menu-item>
</el-menu>
</div>
</template>

静态转动态菜单

最后利用后端获取的 admin 权限数据将静态菜单改为动态菜单:

src/views/Home.vue

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
<template>
<div class="home">
<el-menu default-active="2" class="el-menu-vertical-demo" :router="true">
<template v-for="m in menuList">
<el-submenu index="1-1" :key="m.auth" v-if="m.children">
<template slot="title">{{m.name}}</template>
<el-menu-item index="1-1-1">选项1-1-1</el-menu-item>
<el-menu-item index="1-1-2">选项1-1-2</el-menu-item>
</el-submenu>
<el-menu-item index="1-2" :key="m.auth" v-else>{{m.name}}</el-menu-item>
</template>
<!-- <el-submenu index="1">
<template slot="title">导航一</template>
<el-submenu index="1-1">
<template slot="title">选项1-1</template>
<el-menu-item index="1-1-1">选项1-1-1</el-menu-item>
<el-menu-item index="1-1-2">选项1-1-2</el-menu-item>
</el-submenu>
<el-menu-item index="1-2">选项1-2</el-menu-item>
</el-submenu>
<el-menu-item index="2">
导航二
</el-menu-item>
<el-menu-item index="3">
导航三
</el-menu-item>
<el-menu-item index="4">
导航四
</el-menu-item> -->
</el-menu>
</div>
</template>

<script>
import { mapState } from 'vuex';
export default {
name: 'home',
computed: {
...mapState(['menuList'])
}
};
</script>

无限菜单

仔细观察上方的菜单格式,会发现下面这段代码会重复出现:

1
2
3
4
5
6
7
8
<!-- 带有子菜单的 -->
<el-submenu index="1-1" :key="m.auth" v-if="m.children">
<template slot="title">{{m.name}}</template>
<el-menu-item index="1-1-1">选项1-1-1</el-menu-item>
<el-menu-item index="1-1-2">选项1-1-2</el-menu-item>
</el-submenu>
<!-- 不带有子菜单的 -->
<el-menu-item index="1-2" :key="m.auth" v-else>{{m.name}}</el-menu-item>

所以我们把它提取出去单独渲染,这样就能实现无限菜单了:

新建 src/views/ReSubMenu.vue

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
<template>
<!-- 封装递归组件 -->
<el-submenu :index="data.path">
<!-- 子菜单标题 -->
<template slot="title">
<router-link :to="data.path">{{data.name}}</router-link>
</template>
<template v-for="c in data.children">
<!-- 若子菜单还有children,继续递归自己 -->
<ReSubMenu :key="c.auth" v-if="data.children" :data="c"></ReSubMenu>
<!-- 没有则直接显示 -->
<el-menu-item :key="c.auth" v-else :index="c.path">
{{c.name}}
</el-menu-item>
</template>
</el-submenu>
</template>

<script>
export default {
name: 'ReSubMenu',
props: {
data: {
type: Object,
default: () => ({})
}
}
};
</script>

然后在父组件 Home.vue 中使用:

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
<template>
<div>
<el-menu default-active="2" class="el-menu-vertical-demo" :router="true">
<template v-for="m in menuList">
<!-- 递归组件 -->
<!-- 判断条件:有children的就是带有子菜单的,使用递归组件 -->
<ReSubMenu :data="m" :key="m.auth" v-if="m.children"></ReSubMenu>
<!-- 没有children的情况 -->
<el-menu-item v-else :key="m.auth" :index="m.path">
{{m.name}}
</el-menu-item>
</template>
</el-menu>
<router-view></router-view>
</div>
</template>
<script>
//导入vuex中的方法,具体参考我的文章
import { mapState } from 'vuex';
import ReSubMenu from './ReSubMenu';
export default {
name: 'home',
computed: {
//根据后端传过来的数据来渲染Home
...mapState(['menuList'])
},
components: {
ReSubMenu
}
};
</script>

根据权限动态添加路由

实现了无限菜单后,我们接下来就要根据之前保存的 authList 实现动态路由了。

第一步就是把 router.js 中的路由列表分成两部分,一部分是不需要权限就能访问的,另一部分是需要根据权限列表动态生成的,我们首先来抽离之前在 routes 中的路由列表,拷贝一份后存到两个变量中,其中 authRoutes 需要 export 导出,等会 store.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
// 默认路由列表:不需要权限就能访问的
let defaultRoutes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home
},
{
path: '*',
component: () => import('@/views/404.vue')
}
];

// 需要权限才能访问的路由列表
export let authRoutes = [
{
path: '/cart',
name: 'cart',
//使用懒加载,当使用这个组件的时候再加载资源,当组件资源较大时,不建议使用,可能会出现白屏现象
//而且最好使用绝对路径,@是绝对路径的意思,相当于src下
component: () => import('@/components/menu/cart.vue'),
//配置子路由
children: [
{
//当配置子路由时,最好不要在前面加'/',比如:'/cart-list'
path: 'cart-list',
name: 'cart-list',
component: () => import('@/components/menu/cart-list.vue'),
//配置子路由
children: [
{
path: 'lottery',
name: 'lottery',
component: () => import('@/components/menu/lottery.vue')
},
{
path: 'product',
name: 'product',
component: () => import('@/components/menu/product.vue')
}
]
}
]
},
{
path: '/profile',
name: 'profile',
component: () => import('@/components/menu/profile.vue')
},
{
path: '/shop',
name: 'shop',
component: () => import('@/components/menu/shop.vue')
}
];

然后把原本 routes 列表中的数组删除,改为 defaultRoutes

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
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';

Vue.use(Router);

// 默认路由列表:不需要权限就能访问的
let defaultRoutes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home
},
{
path: '*',
component: () => import('@/views/404.vue')
}
];

// 需要权限才能访问的路由列表
export let authRoutes = [
{
path: '/cart',
name: 'cart',
//使用懒加载,当使用这个组件的时候再加载资源,当组件资源较大时,不建议使用,可能会出现白屏现象
//而且最好使用绝对路径,@是绝对路径的意思,相当于src下
component: () => import('@/components/menu/cart.vue'),
//配置子路由
children: [
{
//当配置子路由时,最好不要在前面加'/',比如:'/cart-list'
path: 'cart-list',
name: 'cart-list',
component: () => import('@/components/menu/cart-list.vue'),
//配置子路由
children: [
{
path: 'lottery',
name: 'lottery',
component: () => import('@/components/menu/lottery.vue')
},
{
path: 'product',
name: 'product',
component: () => import('@/components/menu/product.vue')
}
]
}
]
},
{
path: '/profile',
name: 'profile',
component: () => import('@/components/menu/profile.vue')
},
{
path: '/shop',
name: 'shop',
component: () => import('@/components/menu/shop.vue')
}
];

// 需要查看当前用户权限来动态添加路由
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
// 默认所有用户可访问的路由,其他需要权限的动态在 main.js 中动态装载给当前router对象
routes: defaultRoutes
});

再回到 main.js 中的全局钩子函数,我们需要创建一个 action 来动态生成需要权限才能访问的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 只要页面切换就执行的钩子
router.beforeEach(async (to, from, next) => {
// 判断当前有没有获取过权限,如果获取过了,就不要再获取了
if (!store.state.hasRules) {
//获取权限,调用获取权限的接口,去action中获取数据
await store.dispatch('getMenuList');
// 拿到动态生成的路由列表
let r = await store.dispatch('getAuthRoute');
// 添加到当前路由中
router.addRoutes(r);
next();
} else {
//如果已经获取了权限就可以访问页面了
next();
}
});

最后在 store.js 中实现 getAuthRoute 这个 action:

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
...
// 首先导入router中完整的权限路由列表
import { authRoutes } from './router'

Vue.use(Vuex)

...
// 然后在router的所有权限列表中,匹配当前用户可以访问的权限列表
// 生成动态路由
let getNeedRoutes = (auth) => { //['cart','cart-list'....]
function r(authRoutes) {
return authRoutes.filter(route => {
if (auth.includes(route.name)) {
if (route.children) { //如果有儿子
//找到儿子继续看子路由的权限
route.children = r(route.children)
}
return true; //有权限就返回
}
})
}
return r(authRoutes);
}

export default new Vuex.Store({
...
actions: {
..
async getAuthRoute({ commit, state }) {
// 最后把当前需要动态添加的路由返回出去
let r = getNeedRoutes(state.authList)
return r
}
}
})

到此为止就实现了一套 动态路由 + 路由鉴权 功能。

配套代码

动态路由+路由鉴权