vue-element-admin的侧边栏逻辑分析
1.源码位置
sidebar 引用自 layout 组件,layout 组件位于 src/layout/index.vue
sidebar 组件源码位于 src/layout/components/Sidebar/index.vue
2.el-menu 用法解析
侧边栏的核心是将根据权限过滤后的 router 和 el-menu 组件进行映射,所以熟悉 el-menu 是理解 sidebar 的起点
<template> <el-row class="tac"> <el-col :span="12"> <el-menu default-active="1-1" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" mode="vertical" unique-opened :collapse="isCollapse" :collapse-transition="false" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose" @select="handleSelect" > <el-submenu index="1"> <template slot="title"> <i class="el-icon-location">i> <span>导航一span> template> <el-menu-item-group> <template slot="title">分组一template> <el-menu-item index="1-1">选项1el-menu-item> <el-menu-item index="1-2">选项2el-menu-item> el-menu-item-group> <el-menu-item-group title="分组2"> <el-menu-item index="1-3">选项3el-menu-item> el-menu-item-group> <el-submenu index="1-4"> <template slot="title">选项4template> <el-menu-item index="1-4-1">选项1el-menu-item> el-submenu> el-submenu> <el-submenu index="2"> <template slot="title"> <i class="el-icon-menu">i> <span slot="title">导航二span> template> <el-menu-item index="2-1">选项2-1el-menu-item> el-submenu> <el-menu-item index="3" disabled> <i class="el-icon-document">i> <span slot="title">导航三span> el-menu-item> <el-menu-item index="4"> <i class="el-icon-setting">i> <span slot="title">导航四span> el-menu-item> el-menu> el-col> <el-col> <el-button @click="isCollapse = !isCollapse">折叠el-button> el-col> el-row> template> <script> export default { data() { return { isCollapse: false } }, methods: { handleSelect(key, keyPath) { console.log('handleSelect', key, keyPath) }, handleOpen(key, keyPath) { console.log('handleOpen', key, keyPath) }, handleClose(key, keyPath) { console.log('handleClose', key, keyPath) } } } script>
2.1.el-menu
el-menu 表示菜单容器组件:
default-active
:激活的菜单,注意如果存在子菜单,需要填入子菜单 IDunique-opened
:是否保持一个菜单打开mode
:枚举值,分为 vertical 和 horizontal 两种collapse
:是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)collapse-transition
:是否显示折叠动画@select
:点击菜单事件,keyPath 代表菜单的访问路径,如:1-4-1 菜单的点击日志为:
handleSelect 1-4-1 (3) ["1", "1-4", "1-4-1"]
获取 keyPath 我们可以获取 1-4-1 菜单的所有父级菜单的 ID
@open
:父菜单打开时触发事件@close
:父菜单关闭时触发事件
2.2.el-submenu
子菜单容器,el-submenu 与 el-menu 不同,el-menu 表示整个菜单,而 el-submenu 表示一个具体菜单,只是该菜单还包含了子菜单
el-submenu 可以通过定制 slot 的 title 来自定义菜单样式:
<el-submenu index="1"> <template slot="title"> <i class="el-icon-location">i> <span>导航一span> template> el-submenu>
el-submenu 容器内 default 的 slot 用来存放子菜单,可以包含三种子菜单组件:
el-menu-item-group
:菜单分组,为一组菜单添加一个标题,el-menu-item-group
容器内容需要存放el-menu-item
组件,支持通过 title 的 slot 来定制标题样式el-submenu
:el-submenu
支持循环嵌套el-submenu
,这使得超过两级子组件得以实现el-menu-item
:子菜单组件
3.sidebar 源码分析
sidebar 源码如下:
<template> <div :class="{'has-logo':showLogo}"> <logo v-if="showLogo" :collapse="isCollapse" /> <el-scrollbar wrap-class="scrollbar-wrapper"> <el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg" :text-color="variables.menuText" :unique-opened="false" :active-text-color="variables.menuActiveText" :collapse-transition="false" mode="vertical" > <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" /> el-menu> el-scrollbar> div> template> <script> import { mapGetters } from 'vuex' import Logo from './Logo' import SidebarItem from './SidebarItem' import variables from '@/styles/variables.scss' export default { components: { SidebarItem, Logo }, computed: { ...mapGetters([ 'permission_routes', 'sidebar' ]), activeMenu() { const route = this.$route const { meta, path } = route if (meta.activeMenu) { return meta.activeMenu } return path }, showLogo() { return this.$store.state.settings.sidebarLogo }, variables() { return variables }, isCollapse() { return !this.sidebar.opened } } } script>
- activeMenu:通过 meta.activeMenu 属性,指定路由对应的高亮菜单,meta.activeMenu 需要提供一个合法的路由,否则将不能生效
- isCollapse:NavBar 中点击按钮,会修改 Cookie 中的 sidebarStatus,从 vuex 取值时会将 sidebarStatus 转为 Boolean,并判断默认是否需要收缩左侧菜单栏
- showLogo:判断 settings.js 中的配置项是否需要展示 Logo
- variables:从
@/styles/variables.scss
中获取 scss 对象,从而获取样式
sidebar 中通过 sidebar-item 实现子菜单,下面我们来分析 sidebar-item 组件
4.sidebar-item 源码分析
side-item 组件源码如下:
<template> <div v-if="!item.hidden" class="menu-wrapper"> <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> el-menu-item> app-link> template> <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> <template slot="title"> <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> template> <sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> el-submenu> div> template> <script> import path from 'path' import { isExternal } from '@/utils/validate' import Item from './Item' import AppLink from './Link' import FixiOSBug from './FixiOSBug' export default { name: 'SidebarItem', components: { Item, AppLink }, mixins: [FixiOSBug], props: { // route object item: { type: Object, required: true }, isNest: { type: Boolean, default: false }, basePath: { type: String, default: '' } }, data() { // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237 // TODO: refactor with render function this.onlyOneChild = null return {} }, methods: { hasOneShowingChild(children = [], parent) { const showingChildren = children.filter(item => { if (item.hidden) { return false } else { // Temp set(will be used if only has one showing child) this.onlyOneChild = item return true } }) // When there is only one child router, the child router is displayed by default if (showingChildren.length === 1) { return true } // Show parent if there are no child router to display if (showingChildren.length === 0) { this.onlyOneChild = { ... parent, path: '', noShowingChildren: true } return true } return false }, resolvePath(routePath) { if (isExternal(routePath)) { return routePath } if (isExternal(this.basePath)) { return this.basePath } return path.resolve(this.basePath, routePath) } } } script>
4.1.side-item props 分析
side-item 的 props 如下:
- item:路由对象
- basePath:路由路径
4.2.sidebar-item 展示逻辑分析
sidebar-item 最重要是展示逻辑,主要分为以下几步:
- 通过 item.hidden 控制菜单是否展示
- 通过
hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow
逻辑判断 template 菜单是否展示,template 代表单一菜单;hasOneShowingChild
:判断是否只有一个需要展示的子路由!onlyOneChild.children||onlyOneChild.noShowingChildren
:判断需要展示的子菜单,是否包含 children 属性,如果包含,则说明子菜单可能存在孙子菜单,此时则需要再判断 noShowingChildren 属性!item.alwaysShow
:判断路由中是否存在 alwaysShow 属性,如果存在,则返回 false,不展示 template 菜单,也就说只要配置了 alwaysShow 属性就会直接进入 el-submenu 组件
hasOneShowingChild
方法源码详解
入参:
- children:router 对象的 children 属性
- item:router 对象
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
// 如果 children 中的路由包含 hidden 属性,则返回 false
if (item.hidden) {
return false
} else {
// 将子路由赋值给 onlyOneChild,用于只包含一个路由时展示
this.onlyOneChild = item
return true
}
})
// 如果过滤后,只包含展示一个路由,则返回 true
if (showingChildren.length === 1) {
return true
}
// 如果没有子路由需要展示,则将 onlyOneChild 的 path 设置空路由,并添加 noShowingChildren 属性,表示虽然有子路由,但是不需要展示子路由
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
// 返回 false,表示不需要展示子路由,或者超过一个需要展示的子路由
return false
}
- 如果展示 template 组件,首先会展示 app-link 组件,然后是 el-menu-item,最里面嵌套的是 item 组件:
item 组件需要路由 meta 中包含 title 和 icon 属性,否则将渲染内容为空的 vnode 对象
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> el-menu-item> app-link>
- 如果 template 菜单不展示,则展示 el-submenu 菜单,el-submenu 逻辑中采用了嵌套组件的做法,将 sidebar-item 嵌套在 el-submenu 中:
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> <template slot="title"> <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> template> <sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> el-submenu>
el-submenu 中的 sidebar-item 有两点区别:
- 第一是传入 is-nest 参数
- 第二是传入 base-path 参数
5.app-link 源码分析
app-link 是一个动态组件,通过解析 to 参数,如果包含 http 前缀则变成一个 a 标签,否则变成一个 router-link 组件
<template> <component v-bind="linkProps(to)"> <slot /> component> template> <script> import { isExternal } from '@/utils/validate' export default { props: { to: { type: String, required: true } }, methods: { linkProps(url) { if (isExternal(url)) { return { is: 'a', href: url, target: '_blank', rel: 'noopener' } } return { is: 'router-link', to: url } } } } script>
isExternal
函数通过一个正则表达式匹配 http 链接:
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
6.item 组件源码分析
item 组件通过定义 render 函数完成组件渲染
<script> export default { name: 'MenuItem', functional: true, props: { icon: { type: String, default: '' }, title: { type: String, default: '' } }, render(h, context) { const { icon, title } = context.props const vnodes = [] if (icon) { vnodes.push(<svg-icon icon-class={icon}/>) } if (title) { vnodes.push(<span slot='title'>{(title)}</span>) } return vnodes } } script>
7.总结
- sidebar:sidebar 主要包含 el-menu 容器组件,el-menu 中遍历 vuex 中的 routes,生成 sidebar-item 组件。sidebar 主要配置项如下:
- activeMenu:根据当前路由的 meta.activeMenu 属性控制侧边栏中高亮菜单
- isCollapse:根据 Cookie 的 sidebarStatus 控制侧边栏是否折叠
- variables:通过
@/styles/variables.scss
填充 el-menu 的基本样式
- sidebar-item:sidebar-item 分为两部分:
- 第一部分是当只需要展示一个 children 或者没有 children 时进行展示,展示的组件包括:
- app-link:动态组件,path 为链接时,显示为 a 标签,path 为路由时,显示为 router-link 组件
- el-menu-item:菜单项,当 sidebar-item 为非 nest 组件时,el-menu-item 会增加 submenu-title-noDropdown 的 class
- item:el-menu-item 里的内容,主要是 icon 和 title,当 title 为空时,整个菜单项将不会展示
- 第二部分是当 children 超过两项时进行展示,展示的组件包括:
- el-submenu:子菜单组件容器,用于嵌套子菜单组件
- sidebar-item:el-submenu 迭代嵌套了 sidebar-item 组件,在 sidebar-item 组件中有两点变化:
- 设置 is-nest 属性为 true
- 根据 child.path 生成了 base-path 属性传入 sidebar-item 组件
- 第一部分是当只需要展示一个 children 或者没有 children 时进行展示,展示的组件包括: