vue-element-admin的側邊欄邏輯分析


1.源碼位置

sidebar 引用自 layout 組件,layout 組件位於 src/layout/index.vue
sidebar 組件源碼位於 src/layout/components/Sidebar/index.vue

 

2.el-menu 用法解析

側邊欄的核心是將根據權限過濾后的 routerel-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:激活的菜單,注意如果存在子菜單,需要填入子菜單 ID
  • unique-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-submenuel-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 組件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM