基于Vue3和Element Plus的遞歸組件實(shí)現(xiàn)多級(jí)導(dǎo)航欄功能(示例代碼)
前言
在日常工作中,我能經(jīng)常能碰到那種自引用類似的數(shù)據(jù)結(jié)構(gòu)。比方說(shuō)樹(shù)形的導(dǎo)航欄結(jié)構(gòu),我們期望Vue渲染時(shí)遞歸遍歷組件樹(shù),直到葉子節(jié)點(diǎn)。一個(gè)組件在其自身的模板中調(diào)用自身的組件,這便是Vue中的遞歸組件。
一、遞歸的意義
想象一個(gè)這樣的一個(gè)對(duì)象數(shù)組。每個(gè)對(duì)象有基本的path路徑,name名稱,label標(biāo)簽,icon圖標(biāo)。這四個(gè)屬性是必須的,個(gè)別對(duì)象本身有children屬性包含它的子菜單內(nèi)容。并且children屬性本身也是對(duì)應(yīng)一個(gè)包含path,name,label,icon四個(gè)基本屬性和children可選屬性的對(duì)象。
這樣的話我們就構(gòu)建了一個(gè)典型的自引用類似的數(shù)據(jù)結(jié)構(gòu),相互之間互相嵌套。相信大家已經(jīng)發(fā)現(xiàn)了,在我們不知道嵌套深度的情況下,是沒(méi)法通過(guò)循環(huán)完整解析出全部數(shù)據(jù)。并且遞歸的邏輯復(fù)用性是優(yōu)于循環(huán)。這樣的結(jié)果也會(huì)使遞歸在代碼居然簡(jiǎn)潔性,靈活性,方便擴(kuò)展性。
二、遞歸組件的實(shí)現(xiàn)——基于element-plus UI的多級(jí)導(dǎo)航欄
2.1 element-plus Menu菜單官方示例
我們接下來(lái)要開(kāi)發(fā)的多級(jí)導(dǎo)航欄是基于element-plus的menu菜單組件。觀察官方代碼結(jié)構(gòu),組件最由el-menu標(biāo)簽包裹,el-sub-menu為一級(jí)菜單,內(nèi)嵌的el-menu-item為一級(jí)菜單的子內(nèi)容。但el-sub-menu里面也可以繼續(xù)添加el-sub-menu標(biāo)簽,作為一個(gè)二級(jí)菜單。實(shí)現(xiàn)組件樹(shù)的結(jié)構(gòu)。
樹(shù)狀導(dǎo)航欄代碼如下:
<el-menu> <!-- 最外層菜單容器 -->
├─ <el-sub-menu> <!-- 一級(jí)菜單(可展開(kāi)) -->
│ ├─ 菜單標(biāo)題(含圖標(biāo)和文本)
│ └─ <el-sub-menu> <!-- 二級(jí)菜單(嵌套在一級(jí)菜單內(nèi)) -->
│ ├─ 二級(jí)菜單標(biāo)題
│ └─ <el-menu-item> <!-- 二級(jí)菜單子項(xiàng) -->
├─ <el-menu-item> <!-- 一級(jí)菜單子項(xiàng)(非展開(kāi)項(xiàng)) -->
└─ <el-menu-item> <!-- 一級(jí)菜單子項(xiàng) -->
</el-menu>完整實(shí)例代碼
<template>
<el-row class="tac">
<el-col :span="12">
<h5 class="mb-2">Default colors</h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>2.2 接口定義
定義組件的對(duì)象數(shù)組結(jié)構(gòu)。path路徑,name名稱,label標(biāo)簽,icon圖標(biāo),這四個(gè)屬性是必須的。個(gè)別對(duì)象本身有children屬性包含它的子菜單內(nèi)容。
export interface NavTreeMenuItem {
path: string;
name?: string;
label: string;
icon: string;
children?: NavTreeMenuItem[];
}2.3 組件遞歸
vue3中實(shí)現(xiàn)遞歸組件是組件在其模板中直接調(diào)用自身。比如我定義了一個(gè)組件名稱叫menuTreeItem.vue,我就可以在該vue文件內(nèi)直接調(diào)用menuTreeItem。這是基于這主要?dú)w功于Vue 3 單文件組件的編譯時(shí)處理機(jī)制和 < script setup> 的特性。
在menuTreeItem.vue的el-sub-menu節(jié)點(diǎn)里判斷children屬性是否有值,并且沒(méi)有超過(guò)最大遞歸深度。
值得注意的是,使用遞歸方法要注意無(wú)限遞歸這個(gè)漏洞風(fēng)險(xiǎn),需要設(shè)置最大深度,避免無(wú)限遞歸
menuTreeItem.vue文件片段
<template>
<template v-for="item in treeData" :key="item.path" :index="item.path">
<el-sub-menu v-if="item.children && item.children.length > 0 && currentDepth < maxDepth" :index="item.path">
<!-- -->
<menuTreeItem :tree-data="item.children" :current-depth="currentDepth + 1"
:max-depth="maxDepth"></menuTreeItem>
<!-- -->
</el-sub-menu>
</template>
</template>2.4 父組件封裝遞歸組件
前文我們把需要遞歸的組件封裝成單獨(dú)子組件,最后嵌套在父組件里,防止不必要的內(nèi)容被遍歷渲染。
<template>
<el-aside :style="{ width: menuWidth }">
<h3 class="mb-2" v-once>后臺(tái)系統(tǒng)</h3>
<div class="sidebar-menu">
<el-menu default-active="2" class="my-el-menu" @open="handleOpen" @close="handleClose">
<menuTreeItem :tree-data="treeData" :max-depth="5"></menuTreeItem>
</el-menu>
</div>
</el-aside>
</template>三、完整代碼——基于element-plus UI的多級(jí)導(dǎo)航欄
3.1 組件架構(gòu)
navigationAside
├─ index.vue <!-- 核心組件 -->
├─ menuTreeItem.vue <!-- 遞歸組件 -->
└─ types.ts <!-- 接口類型 -->
</el-menu>3.2 types.ts
export interface NavTreeMenuItem {
path: string;
name?: string;
label: string;
icon: string;
url?: string;
children?: NavTreeMenuItem[];
}3.3 menuTreeItem.vue
<template>
<template v-for="item in treeData" :key="item.path" :index="item.path">
<el-sub-menu v-if="item.children && item.children.length > 0 && currentDepth < maxDepth" :index="item.path">
<template #title>
<component class="icons" :is="item.icon"></component>
<span>{{ item.label }}</span>
</template>
<MenuTreeItem :tree-data="item.children" :isRoot="false" :current-depth="currentDepth + 1"
:max-depth="maxDepth"></MenuTreeItem>
</el-sub-menu>
<el-menu-item v-else :index="item.path">
<component class="icons" :is="item.icon"></component>
<span>{{ item.label }}</span>
</el-menu-item>
</template>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
const router = useRouter()
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
import { NavTreeMenuItem } from './types'
const props = withDefaults(defineProps<{
treeData: NavTreeMenuItem[];
isRoot?: boolean;
currentDepth?: number; // 當(dāng)前深度
maxDepth?: number; // 最大深度
}>(), {
isRoot: true,
currentDepth: 1, // 默認(rèn)當(dāng)前深度為1
maxDepth: 3 // 默認(rèn)最大深度為3
})
</script>
<style lang="less" scoped>
:deep(.icons) {
height: 18px;
margin-right: 5px;
width: 18px;
}
</style>3.4 index.vue
<template>
<el-aside :style="{width: menuWidth}">
<h3 v-if="!isCollapse" class="mb-2" v-once>天津城安遠(yuǎn)傳系統(tǒng)</h3>
<h3 v-else class="mb-2" v-once>遠(yuǎn)傳</h3>
<div class="sidebar-menu">
<el-menu default-active="2"
class="my-el-menu"
:collapse="isCollapse"
@open="handleOpen"
@close="handleClose" >
<menuTreeItem :tree-data="treeData" :max-depth="5"></menuTreeItem>
</el-menu>
</div>
</el-aside>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import {useWebSettingDataStore} from '../../stores'
import menuTreeItem from './menuTreeItem.vue'
const router = useRouter()
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
import { NavTreeMenuItem } from './types'
const props = defineProps<{
treeData: NavTreeMenuItem[];
isRoot?: boolean;
}>();
const store = useWebSettingDataStore()
const isCollapse = computed(() => store.state.isCollapse);
const menuWidth = computed(() => store.state.isCollapse ? '64px' : '180px')
const handleOpen = (key: string, keyPath: string[]) => {
console.log("open")
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log("close")
console.log(key, keyPath)
}
</script>
<style lang="less" scoped>
.el-menu {
border-right: none;
height: 100%;
}
.el-aside {
display: flex;
flex-direction: column;
//border-right: 1px solid #e4e7ed;
background-color: #304156;
h3 {
line-height: 13px;
color: #fff;
text-align: center;
flex-shrink: 0;
/* 防止被壓縮 */
}
}
.sidebar-menu {
flex: 1;
overflow-y: auto;
padding: 10px 0;
}
/* 可選:美化滾動(dòng)條 */
.sidebar-menu::-webkit-scrollbar {
width: 4px;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background-color: #bfcbd9;
border-radius: 3px;
}
.my-el-menu{
--el-menu-bg-color: #304156;
--el-menu-text-color: #bfcbd9;
--el-menu-hover-bg-color: #263445;
--el-menu-active-color: #409eff;
}
</style>3.5 組件調(diào)用與運(yùn)行結(jié)果
<commNavigationAside :tree-data="navTreeData"></commNavigationAside> import commNavigationAside from "@/components/navigationAside/index.vue"

總結(jié)
本文基于 Vue3 和 Element Plus,通過(guò)遞歸組件實(shí)現(xiàn)了可動(dòng)態(tài)渲染的多級(jí)導(dǎo)航欄,利用自引用數(shù)據(jù)結(jié)構(gòu)和深度控制避免無(wú)限循環(huán),同時(shí)結(jié)合 TypeScript 規(guī)范數(shù)據(jù)類型并優(yōu)化組件封裝。
到此這篇關(guān)于基于Vue3和Element Plus的遞歸組件實(shí)現(xiàn)多級(jí)導(dǎo)航欄的文章就介紹到這了,更多相關(guān)vue Element Plus導(dǎo)航欄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue初始化頁(yè)面閃動(dòng)(FOUC)問(wèn)題的終極解決方案
你是否遇到過(guò)頁(yè)面加載瞬間,用戶看到滿屏的 {{ message }}、{{ user.name }} 等未編譯的模板標(biāo)簽,幾毫秒后才恢復(fù)正常,這就是 Vue 初始化閃動(dòng)問(wèn)題,也稱為 FOUC,本文將提供完整、可靠、可落地的解決方案,徹底根除此問(wèn)題,需要的朋友可以參考下2025-10-10
Vite前端API調(diào)用與本地開(kāi)發(fā)代理配置策略詳解
本文詳細(xì)介紹了如何在前端代碼中使用相對(duì)路徑定義后端API URL,并通過(guò)Vite開(kāi)發(fā)服務(wù)器的代理配置,解決了跨域資源共享(CORS)問(wèn)題和環(huán)境差異問(wèn)題,通過(guò)這套組合方案,實(shí)現(xiàn)了同一份前端代碼在本地開(kāi)發(fā)和生產(chǎn)環(huán)境下的無(wú)縫運(yùn)行2025-12-12
vue如何在用戶要關(guān)閉當(dāng)前網(wǎng)頁(yè)時(shí)彈出提示的實(shí)現(xiàn)
這篇文章主要介紹了vue如何在用戶要關(guān)閉當(dāng)前網(wǎng)頁(yè)時(shí)彈出提示的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
vue訪問(wèn)未定義的路由時(shí)重定向404問(wèn)題
這篇文章主要介紹了vue訪問(wèn)未定義的路由時(shí)重定向404問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
vue3實(shí)現(xiàn)按鈕級(jí)別權(quán)限控制的兩種方法
本文給大家介紹vue3實(shí)現(xiàn)按鈕級(jí)別權(quán)限控制的兩種方法,每種方法結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2025-08-08
vue?router?動(dòng)態(tài)路由清除方式
這篇文章主要介紹了vue?router?動(dòng)態(tài)路由清除方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05

