起因 事情是这样的,项目里需要对路由和按钮配置权限,所以会涉及到动态输出路由的问题,然后前端用的是 vue-element-admin
,看项目里是支持动态加载的,但是官方文档里对这块说得…算是语焉不详吧。
然后我翻了翻 issue 挺多人在问的,也有不少人给出了解决方案。嗯,我权当整理一下,怕年纪大容易忘。
思路 因为 vue-element-admin
主要是用于后台管理的框架,所以访问流程大致是这样子的:
发送登录请求
验证成功,返回 token
根据 token 返回当前用户信息,其中包括当前用户的角色,以及所属角色能访问的路由
前端创建一个路由组件映射表,主要作用是将后端返回的 component 的值替换为可执行的对象
将清洗过的路由表和前端存在的路由表合并
重载路由表,完成
其实核心在上面的第 4 条。根据项目 issue 里作者和其他人提供的方法,有两种映射表的创建方式:
前端创建创建一份静态的路由组件映射表
根据后端返回的路由表动态替换
我个人倾向于向后端录入一份路由表,然后再动态匹配。这样子只需要维护一份路由表,而不是一份路由表和一份路由组件映射表。
我目前在使用的 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 }, path : { type : String , required : true }, component : { type : String , default : 'Layout' }, redirect : { type : String , default : 'noRedirect' }, hidden : { type : Boolean , default : false }, 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 }, breadcrumb : { type : Boolean , default : true }, }, 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
,在前端做数据清洗的时候,叶子中的这个值一定要设置为 fasle
,alwaysShow
可以不设置,但是这项一定要设置。
到这里,前端后端的路由表结构就都清楚了。
还有一点顺带提一下,如果是一个显示时没有子菜单的菜单项,数据里,父项是不用设置 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 import Layout from '@/layout' function serverRouterMap (serverRouter ) { const res = [] serverRouter.forEach(item => { const router = { ...item } if (router.component === 'Layout' ) { router.component = Layout } else { let views = router.component views = views.replace(/^\/*/g , '' ) router.component = (resolve ) => require ([`@/views/${views} ` ], resolve) } if (router.children && router.children.length > 0 ) { router.children = serverRouterMap(router.children) } 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 const { roles : role, username, avatar, routes } = datadata.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 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 const { roles } = await store.dispatch('user/getInfo' )const accessRoutes = await store.dispatch('permission/generateRoutes' , roles)const data = await store.dispatch('user/getInfo' )const accessRoutes = await store.dispatch('permission/generateRoutes' , data)
到这里,就全部结束了。 保存后,页面会自动刷新,你应该能看到最新的结果。
issue 中的相关讨论 本文的思路来源于两个 issue 的讨论。
手动维护一份路由组件映射表
动态匹配路由组件映射表
最后,vue-element-admin 的项目地址。
评论