Vue結合路由配置遞迴實現選單欄
阿新 • • 發佈:2020-06-16
![](https://img2020.cnblogs.com/blog/774496/202006/774496-20200616104532964-777257125.png)
>作者:小土豆biubiubiu
>
>部落格園:https://www.cnblogs.com/HouJiao/
>
>掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d
>
>
>微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)
>
>作者文章的內容均來源於自己的實踐,如果覺得有幫助到你的話,可以點贊給個鼓勵或留下寶貴意見
# 前言
在日常開發中,專案中的選單欄都是已經實現好了的。如果需要新增新的選單,只需要在`路由配置`中新增一條路由,就可以實現選單的新增。
相信大家和我一樣,有時候會躍躍欲試自己去實現一個選單欄。那今天我就將自己實現的選單欄的整個思路和程式碼分享給大家。
> 本篇文章重在總結和分享選單欄的一個`遞迴實現方式`,`程式碼的優化`、`選單許可權`等不在本篇文章範圍之內,在文中的相關部分也會做一些提示,有個別不推薦的寫法希望大家不要參考哦。
>
> 同時可能會存在一些細節的功能沒有處理或者沒有提及到,忘知曉。
# 最終的效果
![](https://user-gold-cdn.xitu.io/2020/6/5/172836a2c5e81ee9?w=736&h=803&f=png&s=14432)
本次實現的這個選單欄包含有`一級選單`、`二級選單`和`三級選單`這三種類型,基本上已經可以覆蓋專案中不同的選單需求。
後面會一步一步從易到難去實現這個選單。
# 簡單實現
我們都知道到`element`提供了 [`NavMenu`](https://element.eleme.cn/#/zh-CN/component/menu) 導航選單元件,因此我們直接按照文件將這個選單欄做一個簡單的實現。
基本的佈局架構圖如下:
![](https://user-gold-cdn.xitu.io/2020/6/10/1729d322001c7169?w=576&h=474&f=png&s=10647)
### 選單首頁-menuIndex
首先要實現的是`選單首頁`這個元件,根據前面的佈局架構圖並且參考官方文件,實現起來非常簡單。
```html
```
### 頂部選單欄-topMenu
頂部選單欄主要就是一個`logo`和`產品名稱`。
邏輯程式碼也很簡單,我直接將程式碼貼上。
```html
首頁
員工管理
員工統計
員工管理
考勤管理
考勤統計
考勤列表
異常管理
工時管理
工時統計
工時列表
選項一
選項二
```
> 注意選單的樣式程式碼,設定了`絕對定位`,並且設定`top`、`bottom`使選單高度撐滿螢幕。
此時在看下介面效果。
![](https://user-gold-cdn.xitu.io/2020/6/9/172980541f532620?w=903&h=532&f=png&s=9304)
基本上算是實現了一個簡單的選單佈局。
不過在實際專案在設計的時候,選單欄的內容有可能來自後端給我們返回的資料,其中包含`選單名稱`、`選單圖示`以及`選單之間的層級關係`。
總而言之,我們的選單是動態生成的,而不是像前面那種固定的寫法。因此下面我將實現一個動態生成的選單,選單的資料來源於我們的`路由配置`。
# 結合路由配置實現動態選單
### 路由配置
首先,我將專案的路由配置程式碼貼出來。
```javascript
import Vue from 'vue';
import Router from "vue-router";
// 選單
import MenuIndex from '@/components/menu/menuIndex.vue';
// 首頁
import Index from '@/components/homePage/index.vue';
// 人員統計
import EmployeeStatistics from '@/components/employeeManage/employeeStatistics.vue';
import EmployeeManage from '@/components/employeeManage/employeeManage.vue'
// 考勤
// 考勤統計
import AttendStatistics from '@/components/attendManage/attendStatistics';
// 考勤列表
import AttendList from '@/components/attendManage/attendList.vue';
// 異常管理
import ExceptManage from '@/components/attendManage/exceptManage.vue';
// 工時
// 工時統計
import TimeStatistics from '@/components/timeManage/timeStatistics.vue';
// 工時列表
import TimeList from '@/components/timeManage/timeList.vue';
Vue.use(Router)
let routes = [
// 首頁(儀表盤、快速入口)
{
path: '/index',
name: 'index',
component: MenuIndex,
redirect: '/index',
meta: {
title: '首頁', // 選單標題
icon: 'el-icon-s-home', // 圖示
hasSubMenu: false, // 是否包含子選單,false 沒有子選單;true 有子選單
},
children:[
{
path: '/index',
component: Index
}
]
},
// 員工管理
{
path: '/employee',
name: 'employee',
component: MenuIndex,
redirect: '/employee/employeeStatistics',
meta: {
title: '員工管理', // 選單標題
icon: 'el-icon-user-solid', // 圖示
hasSubMenu: true, // 是否包含子選單
},
children: [
// 員工統計
{
path: 'employeeStatistics',
name: 'employeeStatistics',
meta: {
title: '員工統計', // 選單標題,
hasSubMenu: false // 是否包含子選單
},
component: EmployeeStatistics,
},
// 員工管理(增刪改查)
{
path: 'employeeManage',
name: 'employeeManage',
meta: {
title: '員工管理', // 選單標題
hasSubMenu: false // 是否包含子選單
},
component: EmployeeManage
}
]
},
// 考勤管理
{
path: '/attendManage',
name: 'attendManage',
component: MenuIndex,
redirect: '/attendManage/attendStatistics',
meta: {
title: '考勤管理', // 選單標題
icon: 'el-icon-s-claim', // 圖示
hasSubMenu: true, // 是否包含子節點,false 沒有子選單;true 有子選單
},
children:[
// 考勤統計
{
path: 'attendStatistics',
name: 'attendStatistics',
meta: {
title: '考勤統計', // 選單標題
hasSubMenu: false // 是否包含子選單
},
component: AttendStatistics,
},
// 考勤列表
{
path: 'attendList',
name: 'attendList',
meta: {
title: '考勤列表', // 選單標題
hasSubMenu: false // 是否包含子選單
},
component: AttendList,
},
// 異常管理
{
path: 'exceptManage',
name: 'exceptManage',
meta: {
title: '異常管理', // 選單標題
hasSubMenu: false // 是否包含子選單
},
component: ExceptManage,
}
]
},
// 工時管理
{
path: '/timeManage',
name: 'timeManage',
component: MenuIndex,
redirect: '/timeManage/timeStatistics',
meta: {
title: '工時管理', // 選單標題
icon: 'el-icon-message-solid', // 圖示
hasSubMenu: true, // 是否包含子選單,false 沒有子選單;true 有子選單
},
children: [
// 工時統計
{
path: 'timeStatistics',
name: 'timeStatistics',
meta: {
title: '工時統計', // 選單標題
hasSubMenu: false // 是否包含子選單
},
component: TimeStatistics
},
// 工時列表
{
path: 'timeList',
name: 'timeList',
component: TimeList,
meta: {
title: '工時列表', // 選單標題
hasSubMenu: true // 是否包含子選單
},
children: [
{
path: 'options1',
meta: {
title: '選項一', // 選單標題
hasSubMenu: false // 是否包含子選單
},
},
{
path: 'options2',
meta: {
title: '選項二', // 選單標題
hasSubMenu: false // 是否包含子選單
},
},
]
}
]
},
];
export default new Router({
routes
})
```
在這段程式碼的最開始部分,我們引入了需要使用的元件,接著就對路由進行了配置。
> 此處使用了直接引入元件的方式,專案開發中`不推薦`這種寫法,應該使用`懶載入`的方式
路由配置除了最基礎的`path`、`component`以及`children`之外,還配置了一個`meta`資料項。
```javascript
meta: {
title: '工時管理', // 選單標題
icon: 'el-icon-message-solid', // 圖示
hasSubMenu: true, // 是否包含子節點,false 沒有子選單;true 有子選單
}
```
`meta`資料包含的配置有`選單標題`(`title`)、`圖示的類名`(`icon`)和`是否包含子節點`(`hasSubMenu`)。
根據`title`、`icon`這兩個配置項,可以展示當前選單的`標題`和`圖示`。
`hasSubMenu`表示當前的選單項是否有子選單,如果當前選單包含有子選單(`hasSubMenu`為`true`),那當前選單對應的標籤元素就是`el-submenu`;否則當前選單對應的選單標籤元素就是`el-menu-item`。
> 是否包含子選單是一個非常關鍵的邏輯,我在實現的時候是直接將其配置到了`meta.hasSubMenu`這個引數裡面。
### 根據路由實現多級選單
路由配置完成後,我們就需要根據路由實現選單了。
#### 獲取路由配置
既然要根據路由配置實現多級選單,那第一步就需要獲取我們的路由資料。這裡我使用簡單粗暴的方式去獲取路由配置資料:`this.$router.options.routes`。
> 這種方式也不太適用日常的專案開發,因為無法在獲取的時候對路由做進一步的處理,比如`許可權控制`。
我們在元件載入時列印一下這個資料。
```javascript
// 程式碼位置:src/menu/leftMenu.vue
mounted(){
console.log(this.$router.options.routes);
}
```
列印結果如下。
![](https://user-gold-cdn.xitu.io/2020/6/9/17298b9fb51cbcd1?w=969&h=347&f=png&s=26739)
可以看到這個資料就是我們在`router.js`中配置的路由資料。
為了方便使用,我將這個資料定義到計算屬性中。
```javascript
// 程式碼位置:src/menu/leftMenu.vue
computed: {
routesInfo: function(){
return this.$router.options.routes;
}
}
```
#### 一級選單
首先我們來實現`一級選單`。
主要的邏輯就是迴圈路由資料`routesInfo`,在迴圈的時候判斷當前路由`route`是否包含子選單,如果包含則當前選單使用`el-submenu`實現,否則當前選單使用`el-menu-item`實現。
```html
{{route.meta.title}}
{{route.meta.title}}
```
結果:
![](https://user-gold-cdn.xitu.io/2020/6/10/1729c08cd5956552?w=725&h=414&f=png&s=7831)
可以看到,我們第一級選單已經生成了,`員工管理`、`考勤管理`、`工時管理`這三個選單是有子選單的,所以會有一個下拉按鈕。
![](https://user-gold-cdn.xitu.io/2020/6/10/1729c1049c4b546e?w=290&h=215&f=png&s=4240)
不過目前點開是沒有任何內容的,接下來我們就來實現這三個選單下的`二級選單`。
#### 二級選單
`二級選單`的實現和`一級選單`的邏輯是相同的:迴圈子路由`route.children`,在迴圈的時候判斷子路由`childRoute`是否包含子選單,如果包含則當前選單使用`el-submenu`實現,否則當前選單使用`el-menu-item`實現。
那話不多說,直接上程式碼。
```html
{{route.meta.title}}
{{childRoute.meta.title}}
{{childRoute.meta.title}}
{{route.meta.title}}
```
結果如下:
![](https://user-gold-cdn.xitu.io/2020/6/10/1729c23dc1f7f83e?w=197&h=677&f=png&s=6870)
可以看到`二級選單`成功實現。
#### 三級選單
`三級選單`就不用多說了,和`一級`、`二級`邏輯相同,這裡還是直接上程式碼。
```html
{{route.meta.title}}
{{childRoute.meta.title}}
{{child.meta.title}}
{{child.meta.title}}
{{childRoute.meta.title}}
{{route.meta.title}}
```
![](https://user-gold-cdn.xitu.io/2020/6/10/1729c30472aa105d?w=198&h=783&f=png&s=8189)
可以看到`工時列表`下的`三級選單`已經顯示了。
#### 總結
此時我們已經結合`路由配置`實現了這個動態的選單。
不過這樣的程式碼在邏輯上相關於`三層巢狀`的`for`迴圈,對應的是我們有三層的選單。
假如我們有`四層`、`五層`甚至更多層的選單時,那我們還得在巢狀更多層`for`迴圈。很顯然這樣的方式暴露了前面多層`for`迴圈的缺陷,所以我們就需要對這樣的寫法進行一個改進。
# 遞迴實現動態選單
前面我們一直在說`一級`、`二級`、`三級`選單的實現邏輯都是相同的:迴圈子路由,在迴圈的時候判斷子路由是否包含子選單,如果包含則當前選單使用`el-submenu`實現,否則當前選單使用`el-menu-item`實現。那這樣的邏輯最適合的就是使用`遞迴`去實現。
所以我們需要將這部分共同的邏輯抽離出來作為一個獨立的元件,然後遞迴的呼叫這個元件。
![](https://user-gold-cdn.xitu.io/2020/6/10/1729d1bd29f8fcf6?w=511&h=392&f=png&s=11255)
### 邏輯拆分
```html
{{child.meta.title}}
{{child.meta.title}}
```
需要注意的是,這次抽離出來的元件迴圈的時候直接迴圈的是`route`資料,那這個`route`資料是什麼呢。
我們先看一下前面三層迴圈中迴圈的資料來源分別是什麼。
> 為了看得更清楚,我將前面程式碼中一些不相關的內容進行了刪減。
```html
```
從上面的程式碼可以看到:
一級選單迴圈的是`routeInfo`,即最初我們獲取的路由資料`this.$router.options.routes`,迴圈出來的每一項定義為`route`
二級選單迴圈的是`route.children`,迴圈出來的每一項定義為`childRoute`
三級選單迴圈的是`childRoute.children`,迴圈出來的每一項定義為`child`
按照這樣的邏輯,可以發現`二級選單`、`三級選單`迴圈的資料來源都是相同的,即前一個迴圈結果項的`children`,而一級選單的資料來源於`this.$router.options.routes`。
前面我們抽離出來的`menuItem`元件,迴圈的是`route`資料,即不管是`一層選單`還是`二層`、`三層選單`,都是同一個資料來源,因此我們需要統一資料來源。那當然也非常好實現,我們在呼叫元件的時候,為元件傳遞不同的值即可。
![](https://user-gold-cdn.xitu.io/2020/6/10/1729d1ed8bb46ba3?w=543&h=376&f=png&s=14036)
### 程式碼實現
前面公共元件已經拆分出來了,後面的程式碼就非常好實現了。
首先是抽離出來的`meunItem`元件,實現的是`邏輯判斷`以及`遞迴呼叫自身`。
```html
{{child.meta.title}}
{{child.meta.title}}
```
接著是`leftMenu`元件,呼叫`menuIndex`元件,傳遞原始的路由資料`routesInfo`。
```html
```
最終的結果這裡就不展示了,和我們需要實現的結果是一致的。
# 功能完善
到此,我們`結合路由配置實現了選單欄`這個功能基本上已經完成了,不過這是一個缺乏靈魂的選單欄,因為沒有設定選單的跳轉,我們點選選單欄還無法路由跳轉到對應的`元件`,所以接下來就來實現這個功能。
選單跳轉的實現方式有兩種,第一種是`NavMenu`元件提供的跳轉方式。
![](https://user-gold-cdn.xitu.io/2020/6/12/172a67f9f613d9a6?w=914&h=91&f=png&s=4291)
第二種是在選單上新增`router-link`實現跳轉。
那本次我選擇的是第一種方式實現跳轉,這種實現方式需要兩個步驟才能完成,第一步是啟用`el-menu`上的`router`;第二步是設定導航的`index`屬性。
那下面就來實現這兩個步驟。
### 啟用el-menu上的router
```html
```
### 設定導航的index屬性
首先我將每一個選單標題對應需要設定的`index`屬性值列出來。
> `index`值對應的是每個選單在路由中配置的`path`值
首頁
員工管理
員工統計 index="/employee/employeeStatistics"
員工管理 index="/employee/employeeManage"
考勤管理
考勤統計 index="/attendManage/attendStatistics"
考勤列表 index="/attendManage/attendList"
異常管理 index="/attendManage/exceptManage"
員工統計
員工統計 index="/timeManage/timeStatistics"
員工統計 index="/timeManage/timeList"
選項一 index="/timeManage/timeList/options1"
選項二 index="/timeManage/timeList/options2"
接著在回顧前面遞迴呼叫的元件,導航選單的`index`設定的是`child.path`,為了看清楚`child.path`的值,我將其新增選單標題的右側,讓其顯示到介面上。
```html
{{child.meta.title}} | {{child.path}}
{{child.meta.title}} | {{child.path}}
```
同時將選單欄的寬度由`200px`設定為`400px`。
```html
```
然後我們看一下效果。
![](https://user-gold-cdn.xitu.io/2020/6/12/172a69a48edd0ced?w=402&h=686&f=png&s=13705)
可以發現,`child.path`的值就是當前選單在路由中配置`path`值(`router.js`中配置的`path`值)。
那麼問題就來了,前面我們整理了每一個選單標題對應需要設定的`index`屬性值,就目前來看,現在設定的`index`值是不符合要求的。不過仔細觀察現在選單設定的`index`值和正常值是有一點接近的,只是缺少了上一級選單的`path`值,如果能將`上一級選單`的`path`值和當前選單的`path`值進行一個拼接,就能得到正確的`index`值了。
那這個思路實現的方式依然是在遞迴時將當前選單的`path`作為引數傳遞給`menuItem`元件。
```html
```
將當前選單的`path`作為引數傳遞給`menuItem`元件之後,在下一級選單實現時,就能拿到上一級選單的`path`值。然後元件中將`basepath`的值和當前選單的`path`值做一個拼接,作為當前選單的`index`值。
```html
```
再看一下介面。
![](https://user-gold-cdn.xitu.io/2020/6/12/172a6aedef9afe01?w=401&h=689&f=png&s=17085)
我們可以看到二級選單的`index`值已經沒問題了,但是仔細看,發現`工時管理`-`工時列表`下的兩個三級選單`index`值還是有問題,缺少了`工時管理`這個一級選單的`path`。
那這個問題是因為我們在呼叫元件自身是傳遞的`basepath`有問題。
```html
```
`basepath`傳遞的只是上一級選單的`path`,在遞迴`二級選單`時,`index`的值是`一級選單的path值`+`二級選單的path值`;那當我們遞迴`三級選單`時,`index`的值就是`二級選單的path值`+`三級選單的path值`,這也就是為什麼`工時管理-工時列表`下的兩個三級選單`index`值存在問題。
所以這裡的`basepath`值在遞迴的時候應該是`累積`的,而不只是上一級選單的`path`值。因此藉助遞迴演算法的優勢,`basepath`的值也需要通過`getPath`方法進行處理。
```html
```
最終完整的程式碼如下。
```html
{{child.meta.title}}
{{child.meta.title}}
```
> 刪除其餘用來除錯的程式碼
# 最終效果
文章的最後呢,將本次實現的最終效果在此展示一下。
![](https://user-gold-cdn.xitu.io/2020/6/12/172a7b5068577e17?w=967&h=691&f=gif&s=227839)
> `選項一`和`選項二`這兩個三級選單在路由配置中沒有設定`component`,這兩個菜單只是為了實現三級選單,在最後的結果演示中,我已經刪除了路由中配置的這兩個三級選單
>
> 此處在`leftMenu`元件中為`el-menu`開啟了`unique-opened`
>
> 在`menuIndex`元件中,將左側選單欄的寬度改為`200px`
# 關於
### 作者
小土豆biubiubiu
> 一個努力學習的前端小菜鳥,知識是無限的。堅信只要不停下學習的腳步,總能到達自己期望的地方
>
> 同時還是一個喜歡小貓咪的人,家裡有一隻美短小母貓,名叫土豆
### 部落格園
https://www.cnblogs.com/HouJiao/
### 掘金
https://juejin.im/user/58c61b4361ff4b005d9e894d
### 微信公眾號
土豆媽的碎碎念
> 微信公眾號的初衷是記錄自己和身邊的一些故事,同時會不定期更新一些技術文章
>
> 歡迎大家掃碼關注,一起吸貓,一起聽故事,一起學習前端技術
### 作者寄語
小小總結,歡迎大家指導~
{{name}}
``` 這段程式碼中包含了`父元件`傳遞給`子元件`的兩個資料。 ```javascript props: ['logoPath', 'name'] ``` 這個是父元件`menuIndex`傳遞給子元件`topMenu`的兩個資料,分別是`logo圖示的路徑`和`產品名稱`。 完成後的介面效果如下。 ![](https://user-gold-cdn.xitu.io/2020/6/9/172970c37885815d?w=517&h=102&f=png&s=2684) ### 左側選單欄-leftMenu 首先按照官方文件實現一個簡單的選單欄。 ```html