1. 程式人生 > >Vue結合路由配置遞迴實現選單欄

Vue結合路由配置遞迴實現選單欄

![](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
``` 這段程式碼中包含了`父元件`傳遞給`子元件`的兩個資料。 ```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
``` > 注意選單的樣式程式碼,設定了`絕對定位`,並且設定`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}} ``` 結果: ![](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 {{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 {{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 ``` 需要注意的是,這次抽離出來的元件迴圈的時候直接迴圈的是`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 ``` 接著是`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}} ``` 同時將選單欄的寬度由`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 ``` > 刪除其餘用來除錯的程式碼 # 最終效果 文章的最後呢,將本次實現的最終效果在此展示一下。 ![](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 ### 微信公眾號 土豆媽的碎碎念 > 微信公眾號的初衷是記錄自己和身邊的一些故事,同時會不定期更新一些技術文章 > > 歡迎大家掃碼關注,一起吸貓,一起聽故事,一起學習前端技術 ### 作者寄語 小小總結,歡迎大家指導~