在上一篇文章,我们根据用户角色获取了路由,这一篇文章,我们就根据路由来生成菜单栏,这也是后台管理系统的一个必备功能。
这篇文章将会从路由设计以及递归组件两个方面进行讲解,希望能够对大家有所帮助。
系列文章地址:
硬核Vue响应式原理系列文章:
路由设置
目前网上大部分的实战课程在设置路由时,都会采用如下方式
// 写法1
const routes = [
{
path: '/',
component: () => import('XXX')
children: [
{
path: 'a',
component: () => import('A'),
children [
{
path: 'aa',
component: () => import('AA')
}
]
},
{
path: 'b',
component: () => import('B')
}
]
}
]
写法1符合我们的很直观想法,但是个人感觉这种写法不太清晰,因此笔者采用了写法2
// 写法2
const router = [
// 路由1
{
path: '/a',
redirect: '/a/aa',
// 布局组件
component: Layout,
children: [
{
path: 'aa',
component: () => import('AA'),
},
{
path: 'bb',
component: () => import('BB'),
}
]
},
// 路由2
{
// ...
}
// ...
]
个人感觉这种写法的优点在于,每个路由对象对应一个菜单项,结构比较清晰。
其实这两种写法对于菜单的生成是没有影响的,大家可以自行选择。
几个路由参数
alwaysShow // 默认为false,当菜单栏只有一个子项时,会直接显示为一个el-menu-item
// 如果设置为true,则该路由对应的菜单栏为el-submenu
hidden: // 是否在菜单栏中显示该路由,比如/login.hidden = true
meta: {
icon // 菜单栏图标
title // 菜单栏标题
}
递归生成菜单栏
之前我们已经获取到了权限路由,可以定义一个getter来获取它
// src/store/getters.js
const getters = {
// ...
permission_routes: state => state.permission.routes
// ...
}
这样,我们就能通过路由生成菜单栏。
我们的菜单栏组件定义在src/Layout/SideBar文件夹内
首先看SideBar/index.vue,过滤出可以显示的项
created() {
this.routes = this.permissionRoutes.filter(r => !r.hidden)
}
之后循环this.routes生成菜单
<el-menu
router
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:sidebarItemRoute="route"
:base-path="route.path"
/>
</el-menu>
el-menu开启了router模式,这样每个el-menu-item的index就表示path
sidebar-item是自定义的组件,表示每个菜单项,该组件接收两个props
props: {
sidebarItemRoute: {
type: Object,
required: true
},
basePath: {
type: String,
default: ''
}
}
菜单项组件
sidebar-item是一个递归组件
<template v-if="showAsMenuItem && !sidebarItemRoute.alwaysShow">
<el-menu-item :index="resolvePath(menuItemRoute.path)">
<i :class="menuItemRoute.meta.icon"></i>
<span>{{ menuItemRoute.meta.title }}</span>
</el-menu-item>
</template>
<el-submenu v-else :index="resolvePath(sidebarItemRoute.path)">
<template slot="title">
<i :class="sidebarItemRoute.meta.icon"></i>
<span>{{ sidebarItemRoute.meta.title }}</span>
</template>
<sidebar-item
v-for="child in sidebarItemRoute.children"
:key="child.path"
:sidebarItemRoute="child"
:base-path="resolvePath(child.path)"
/>
</el-submenu>
当showAsMenuItem === true并且alwaysShow === false时,当前路由展示为一个el-menu-item。showAsMenuItem的判断在handleRoute方法中,基本逻辑为,如果当前路由没有子路由,则showAsMenuItem为true,或者当前路由只有一个子路由,并且这个子路由没有子路由时,则showAsMenuItem为true
handleRoute() {
// 如果没有子路由,递归结束
if (!this.sidebarItemRoute.children) {
this.showAsMenuItem = true
return
}
// 过滤掉children中hidden的
const showingChildren = this.sidebarItemRoute.children.filter(
item => !item.hidden
)
// 当只有一个子路由,并且这个子路由没有children属性时
if (showingChildren.length === 1 && !showingChildren[0].children) {
this.showAsMenuItem = true
this.menuItemRoute = showingChildren[0]
}
}
另外一个需要处理的就是path,处理思路如下,父组件传递一个base-path,子组件将base-path与自己的path拼接得到完整的路径。因此我们能在模板中看到这样的代码:<el-menu-item :index="resolvePath(menuItemRoute.path)">和<el-submenu v-else :index="resolvePath(sidebarItemRoute.path)">
// js
resolvePath(routePath) {
return path.resolve(this.basePath, routePath)
}
但是有一个特殊情况需要处理,就是当路由没有子路由时,它的base-path就已经是完整的path了,但是在模板中需要将base-path与path进行拼接,因此需要将它自己的path重置为空字符串
handleRoute() {
if (!this.sidebarItemRoute.children) {
this.showAsMenuItem = true
// 如果没有子路由,basePath就是这一层的路由,因此拼接一个空字符串
this.menuItemRoute = {
...this.sidebarItemRoute,
path: ''
}
return
}
// ...
}
这样就OK了。
一个小BUG
一个Vue组件必须有且仅有一个根元素,一般我们会使用div,但是在sidebar-item组件中使用div作为根元素会有一个bug,就是当我们点击菜单收缩时,菜单并没有收缩,这是因为我们把sidebar组件和sidebar-item组件分开写,导致el-menu和el-sub-menu,el-menu-item之间多了一层div,导致element-ui内部的样式出现了问题,我们可以采用一个插件来解决:vue-fragment
// main.js
import Fragment from 'vue-fragment'
Vue.use(Fragment.Plugin)
在sidebar-item组件内以<fragment>标签作为根元素就可以了。如果你有其他的解决方案,也可以评论告诉我!!