前端初学者的第一个Vue后台管理项目总结3:根据路由生成菜单栏

3,389 阅读4分钟

在上一篇文章,我们根据用户角色获取了路由,这一篇文章,我们就根据路由来生成菜单栏,这也是后台管理系统的一个必备功能。

这篇文章将会从路由设计以及递归组件两个方面进行讲解,希望能够对大家有所帮助。

系列文章地址:

1. 项目基础架构

2. 登录与权限控制

硬核Vue响应式原理系列文章:

基本原理

数组的处理

渲染watcher

最终章

路由设置

目前网上大部分的实战课程在设置路由时,都会采用如下方式

// 写法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-itemindex就表示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-itemshowAsMenuItem的判断在handleRoute方法中,基本逻辑为,如果当前路由没有子路由,则showAsMenuItemtrue,或者当前路由只有一个子路由,并且这个子路由没有子路由时,则showAsMenuItemtrue

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-pathpath进行拼接,因此需要将它自己的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-menuel-sub-menu,el-menu-item之间多了一层div,导致element-ui内部的样式出现了问题,我们可以采用一个插件来解决:vue-fragment

// main.js
import Fragment from 'vue-fragment'
Vue.use(Fragment.Plugin)

sidebar-item组件内以<fragment>标签作为根元素就可以了。如果你有其他的解决方案,也可以评论告诉我!!