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">選項1</el-menu-item> <el-menu-item index="1-2">選項2</el-menu-item> </el-menu-item-group> <el-menu-item-group title="分組2"> <el-menu-item index="1-3">選項3</el-menu-item> </el-menu-item-group> <el-submenu index="1-4"> <template slot="title">選項4</template> <el-menu-item index="1-4-1">選項1</el-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-1</el-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> <!-- eslint-disable vue/require-component-is --> <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 時進行展示,展示的組件包括: