在 vue-element-admin 怎样动态生成路由配置?

起因

事情是这样的,项目里需要对路由和按钮配置权限,所以会涉及到动态输出路由的问题,然后前端用的是 vue-element-admin ,看项目里是支持动态加载的,但是官方文档里对这块说得…算是语焉不详吧。

然后我翻了翻 issue 挺多人在问的,也有不少人给出了解决方案。嗯,我权当整理一下,怕年纪大容易忘。

思路

因为 vue-element-admin 主要是用于后台管理的框架,所以访问流程大致是这样子的:

  1. 发送登录请求
  2. 验证成功,返回 token
  3. 根据 token 返回当前用户信息,其中包括当前用户的角色,以及所属角色能访问的路由
  4. 前端创建一个路由组件映射表,主要作用是将后端返回的 component 的值替换为可执行的对象
  5. 将清洗过的路由表和前端存在的路由表合并
  6. 重载路由表,完成

其实核心在上面的第 4 条。根据项目 issue 里作者和其他人提供的方法,有两种映射表的创建方式:

  1. 前端创建创建一份静态的路由组件映射表
  2. 根据后端返回的路由表动态替换

我个人倾向于向后端录入一份路由表,然后再动态匹配。这样子只需要维护一份路由表,而不是一份路由表和一份路由组件映射表。

我目前在使用的 vue-element-admin 版本,可能和 issue 里大家讨论时的版本有比较大的差异,所以接下来的过程和代码,建立在 vue-element-admin version: 4.2.1 上。

准备工作:路由表的前端结构和数据库中数据结构

路由表的前端结构

先来看看框架的路由表长啥样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
path: '/example',
component: Layout,
redirect: '/example/table',
name: 'Example',
meta: { title: 'Example', icon: 'example', roles: ['admin', 'editor'] },
children: [
{
path: 'tree',
name: 'Tree',
component: () => import('@/views/tree/index'),
meta: { title: 'Tree', icon: 'tree' }
},
{
path: 'form',
name: 'Form',
component: () => import('@/views/form/index'),
meta: { title: 'Form', icon: 'form' }
}
]
}

这个就是一个拥有两个子菜单项的路由表。下面这个是另一种形式的。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
path: '/icon',
component: Layout,
alwaysShow: true,
children: [
{
path: 'index',
component: () => import('@/views/icons/index'),
name: 'Icons',
meta: { title: 'icons', icon: 'icon', noCache: true }
}
]
}

这是个只显示一个主菜单而没有子菜单项的路由表。当然,框架中并不只这两种方式,这只是我们需要的两种方式。

提示:再留意一下这两段数据中,都有个叫 component 的字段,表示的是,路由绑定的视图,这个应该都看得懂不用多作解释了。然后这里就是动态生成路由表的关键之处。我把这个字段的意思表示为 路由组件映射

component 的值,如果是从后端返回的,一定会是字符串,而且这时候,前端代码也已经由 webpack 打包好了,所以没办法起作用。那么就需要用映射表的方法,异步加载,让视图生效,从而实现动态加载路由需求。

路由表在数据库中的数据结构

那么要把路由和权限结合起来,当然不能纯人工去维护,所以要在后端保存一份路由表,而且还指定了权限角色的。翻看了 vue-element-admin 的文档和代码,发现路由支持深度嵌套,而且父、子路由的结构是高度相似的,并没有做一个明显的区分,基于这个原因,父子的数据结构将是完全一致的,其中只有两个字段的赋值区别,下面再说。

鉴于此,以及框架支持路由深度嵌套,所以最后的菜单项,都可能是别的菜单的父或者子。这方面在这篇文不深入讨论。

因为我用的 mongodb ,我还是希望用比较平的结构来处理路由,下面是我的 schema 定义。

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
const MenuSchema = new mongoose.Schema({
__v: { type: Number, select: false },
name: { type: String, required: true }, //设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
path: { type: String, required: true },
component: { type: String, default: 'Layout' },
redirect: { type: String, default: 'noRedirect' }, //重定向地址,在面包屑中点击会重定向去的地址
hidden: { type: Boolean, default: false }, // 不在侧边栏显示
// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: { type: Boolean, default: true }, //一直显示根路由
meta: {
role: [{
type: mongoose.Schema.Types.ObjectId, ref: 'Role',
}], //设置该路由进入的权限,支持多个权限叠加。你可以在根路由设置权限,这样它下面所有的子路由都继承了这个权限
title: { type: String, required: true }, //设置该路由在侧边栏和面包屑中展示的名字
icon: { type: String }, //设置该路由的图标
noCache: { type: Boolean, default: false }, //如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
breadcrumb: { type: Boolean, default: true }, // 如果设置为false,则不会在breadcrumb面包屑中显示
},
children: [{
type: mongoose.Schema.Types.ObjectId, ref: 'Menu',
autopopulate: { maxDepth: 3 }
}],
extra: { type: mongoose.Schema.Types.Mixed },
createdAt: { type: Date, default: Date.now }
}, {
id: false,
toObject: { virtuals: true },
toJSON: { virtuals: true }
})

MenuSchema.virtual('meta.roles', {
ref: 'Role',
localField: 'meta.role',
foreignField: '_id',
justOne: false,
autopopulate: { select: 'name', maxDepth: 1 }
})

MenuSchema.plugin(require('mongoose-autopopulate'))

注意:这里要特别说明一下,因为这里的路由表是树状结构,最末尾的叶子,也就是最底层的 child 和 parent 中有两个字段的值是有区分的。一个是 alwaysShow,如果是叶子建议设置为 false ;另一个是redirect,在前端做数据清洗的时候,叶子中的这个值一定要设置为 faslealwaysShow 可以不设置,但是这项一定要设置。

到这里,前端后端的路由表结构就都清楚了。

还有一点顺带提一下,如果是一个显示时没有子菜单的菜单项,数据里,父项是不用设置 name 的.

建立路由组件映射表

对于这个映射表,我们有两种常用的方法来解决。

由前端维护一份静态路由组件映射表

这是交由前端来维护的一份与后端一样的组件映射表。

# src/router/index.js
1
2
3
4
export const routerMap={
'Tree': () => import('@/views/tree/index'),
'Form': () => import('@/views/form/index')
}

面对这份路由组件映射表,实在是高兴不起来。既然后端已经保存有一份了,何苦再来手动维护一份映射表呢?所以还是写个方法,每次获得路由表的时候,都自动去匹配一次就好。所以也就有了下面这个动态替换的方法。

根据后端返回的路由表动态替换路由组件映射

开发人员都是要想办法偷懒的,要不然不会有技术的进步。所以我们决定放弃手动维护一份映射表。

# src/store/modules/user.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
/* Layout */
import Layout from '@/layout'

/**
* 动态生成路由组件映射表并替换
*
* @param {Array} serverRouterMap
* @returns
*/
function serverRouterMap(serverRouter) {
const res = []

serverRouter.forEach(item => {
const router = { ...item }
if (router.component === 'Layout') {
// Layout 要预先 import 进来
router.component = Layout
} else {
let views = router.component
views = views.replace(/^\/*/g, '')
// 异步加载视图
// router.component = () => import(`@/views/${views}`)
// 更新了某个版本的 webpack 后,只能用这种写法了
router.component = (resolve) => require([`@/views/${views}`], resolve)
}

if (router.children && router.children.length > 0) {
router.children = serverRouterMap(router.children)
}

// 如果是最后的叶子节点,把 redirect 设置为 false
if (router.redirect === 'noRedirect' && router.children && router.children.length === 0) {
router.redirect = false
}

res.push(router)
})
return res
}

到这里,主要的方法就搞定了,接下来按登录的流程来生成动态路由。

生成动态路由

要生成动态路由,还是要先拿到数据。在 src/store/modules/user.js 的 actions 里找到 getInfo方法,因为和当前用户相关的数据,都是从这里拿的。

# src/store/modules/user.js
1
2
3
4
5
// 从返回的数据中拿到 routes 数据
const { roles: role, username, avatar, routes } = data

// 然后在合适的地方,调用 serverRouterMap() 方法,清洗并匹配路由组件映射后,重新赋值给 data.routes
data.routes = serverRouterMap(routes)

接下来,小小修改一下 generateRoutes() 方法。修改了接收的参数以及路由数据的来源,和你本地没修改过的方法,对照着改吧。

# src/store/modules/permission.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
generateRoutes({ commit }, data) {
return new Promise(resolve => {
let accessedRoutes
const { routes: serverAsyncRouterMap, roles } = data

// generateRoutes 原来的写法
// if (roles.includes('admin')) {
// accessedRoutes = asyncRoutes || []
// } else {
// accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
// }
// 获取动态路由的写法
if (roles.includes('admin')) {
accessedRoutes = serverAsyncRouterMap
} else {
accessedRoutes = filterAsyncRoutes(serverAsyncRouterMap, roles)
}
commit('SET_ROUTES', accessedRoutes)

resolve(accessedRoutes)
})
}

接着就要 dispatch generateRoutes 来生成路由啦!

# src/permission.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// get user info
// 修改前的样子
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const { roles } = await store.dispatch('user/getInfo')

// generate accessible routes map based on roles
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

// 修改后的样子
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const data = await store.dispatch('user/getInfo')

// generate accessible routes map based on roles
const accessRoutes = await store.dispatch('permission/generateRoutes', data)

到这里,就全部结束了。
保存后,页面会自动刷新,你应该能看到最新的结果。

issue 中的相关讨论

本文的思路来源于两个 issue 的讨论。

手动维护一份路由组件映射表

动态匹配路由组件映射表

最后,vue-element-admin 的项目地址。

Guide for changing dev environment from Mac to Windows Create video with Moviepy

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×