Code前端首页关于Code前端联系我们

Vue3 如何打造灵活易用的菜单组件?从基础搭建到场景拓展一次说透

terry 22小时前 阅读数 222 #Vue
文章标签 菜单组件

做后台管理系统、导航栏的时候,菜单组件几乎是刚需,Vue3 生态下,怎么做出既灵活又好维护的菜单?从最基础的结构搭建,到多级嵌套、路由联动、权限控制这些实际场景,今天一次性聊明白。

基础篇:怎么用Vue3搭出最简化的菜单组件?

先从最基础的“数据驱动菜单”说起,Vue3 的组件化 + 响应式,能让菜单结构和数据彻底解耦,咱可以把菜单数据存在数组里,用组件接收数据后循环渲染。

步骤拆解

  1. 定义组件接收菜单数据,用 defineProps 声明 menuList,数组里每个对象包含 label(显示文本)、key(唯一标识)这些基础字段。
  2. 循环渲染菜单项,用 v-for 遍历 menuList,给每个菜单项绑定点击事件,把点击逻辑通过 emit 抛给父组件。

看个极简示例:

<!-- BaseMenu.vue -->
<template>
  <ul class="base-menu">
    <li 
      v-for="item in menuList" 
      :key="item.key" 
      @click="handleClick(item)"
    >
      {{ item.label }}
    </li>
  </ul>
</template>
<script setup>
const props = defineProps({
  menuList: {
    type: Array,
    required: true,
    default: () => []
  }
})
const emit = defineEmits(['menu-click'])
const handleClick = (item) => {
  emit('menu-click', item)
}
</script>
<style scoped>
.base-menu { list-style: none; padding: 0; }
.base-menu li { padding: 8px 16px; cursor: pointer; }
.base-menu li:hover { background: #f5f7fa; }
</style>

为什么这么做? 把菜单数据和渲染逻辑封装成组件后,父组件只用传 menuList 就能生成菜单,后续改样式、加功能都不用动结构,维护起来特方便。

进阶篇:多级嵌套菜单怎么实现?递归组件来帮忙

实际项目里,菜单往往是多级的(系统设置”下还有“用户管理”“角色管理”),这种嵌套结构,递归组件 是最优解——因为子菜单的结构和父菜单完全一样,组件可以自己调用自己。

实现思路

  1. 新建可递归的菜单项组件(MenuItem.vue),判断当前菜单项是否有 children 字段。
  2. 若有子菜单,就在组件内部渲染自身(递归调用),把 children 当作新的 menuList 传入。

看代码逻辑:

<!-- MenuItem.vue(递归处理子菜单) -->
<template>
  <li class="menu-item">
    <!-- 菜单项标题 + 展开/收起按钮 -->
    <div @click="toggleCollapse">
      {{ item.label }} 
      <span v-if="item.children" class="arrow">▾</span>
    </div>
    <!-- 子菜单区域:用Transition做展开动画 -->
    <Transition name="slide">
      <ul v-show="isOpen" class="sub-menu" v-if="item.children">
        <MenuItem 
          v-for="child in item.children" 
          :key="child.key" 
          :item="child"
        />
      </ul>
    </Transition>
  </li>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
  item: {
    type: Object,
    required: true
  }
})
const isOpen = ref(false) // 控制子菜单展开/收起
const toggleCollapse = () => {
  isOpen.value = !isOpen.value
}
</script>
<style scoped>
.menu-item { padding: 8px 16px; }
.sub-menu { padding-left: 16px; }
.arrow { margin-left: 8px; }
/* 展开收起动画 */
.slide-enter-from, .slide-leave-to {
  height: 0; opacity: 0;
}
.slide-enter-to, .slide-leave-from {
  height: auto; opacity: 1;
}
.slide-enter-active, .slide-leave-active {
  transition: all 0.3s ease;
}
</style>

然后在父组件 Menu.vue 里循环渲染 MenuItem

<template>
  <ul class="menu">
    <MenuItem 
      v-for="item in menuList" 
      :key="item.key" 
      :item="item"
    />
  </ul>
</template>
<script setup>
import MenuItem from './MenuItem.vue'
defineProps({
  menuList: {
    type: Array,
    required: true
  }
})
</script>

关键点:递归组件的核心是“自己调用自己”,但要注意终止条件(比如没有 children 时不再渲染子菜单),这样不管菜单有多少层级,都能自动处理,不用写死嵌套层数。

交互体验:怎么给菜单加展开动画和选中态?

光有结构还不够,交互体验得跟上,比如子菜单展开时的滑动动画、点击后高亮的选中态,这些细节能让组件更“丝滑”。

展开收起动画:用Vue Transition组件

Vue 的 <Transition> 内置组件能轻松实现动画,像上面 MenuItem.vue 里的例子,把要动画的元素(子菜单 ul)用 <Transition> 包裹,再写几行CSS过渡样式,就能实现平滑的展开/收起效果。

选中态:用Provide/Inject管理状态

菜单的“选中高亮”需要跨组件通信(父菜单和子菜单都要知道当前选中项),Vue3 的 provide/inject 很适合这种场景:

  • 父组件 Menu.vueprovide 暴露当前选中的 currentKey(响应式数据)。
  • 子组件 MenuItem.vueinject 接收 currentKey,对比自身 item.key,匹配则添加 active 类。

代码示例(简化版):

<!-- Menu.vue(提供选中状态) -->
<script setup>
import { provide, ref } from 'vue'
import MenuItem from './MenuItem.vue'
const currentKey = ref('')
provide('currentKey', currentKey) // 提供响应式的currentKey
// 点击菜单项时,更新currentKey
const handleMenuItemClick = (key) => {
  currentKey.value = key
}
</script>
<!-- MenuItem.vue(注入并判断选中) -->
<script setup>
import { inject, computed } from 'vue'
const currentKey = inject('currentKey')
const props = defineProps({ item: Object })
// 计算是否为当前选中项
const isActive = computed(() => {
  return props.item.key === currentKey.value
})
</script>
<template>
  <li :class="{ active: isActive }">...</li>
</template>
<style scoped>
.active { background: #eaf2ff; }
</style>

这样不管多少级菜单,只要点击就会更新 currentKey,对应菜单项自动高亮,状态管理特别丝滑。

路由整合:菜单怎么和Vue Router联动?

后台系统里,菜单点击要跳转到对应页面,还得根据当前路由高亮菜单,这时候和 Vue Router 联动是关键。

实现步骤

  1. 给菜单项加 to 字段(对应路由的 path)。
  2. 点击时用 useRouter 跳转,匹配路由时用 useRoute 获取当前路径。

看代码:

<!-- MenuItem.vue 处理路由跳转 -->
<script setup>
import { useRouter, useRoute, computed } from 'vue-router'
const router = useRouter()
const route = useRoute()
const props = defineProps({ item: Object })
// 点击跳转
const handleClick = () => {
  if (props.item.to) {
    router.push({ path: props.item.to })
  }
}
// 自动匹配当前路由,高亮菜单
const isActive = computed(() => {
  return route.path === props.item.to
})
</script>
<template>
  <li 
    @click="handleClick" 
    :class="{ active: isActive }"
  >
    {{ item.label }}
  </li>
</template>

好处:和 Vue Router 生态深度结合后,菜单不用手动维护“哪个页面该高亮”,路由变了自动匹配,点击也能直接跳转,开发效率拉满。

权限控制:不同角色怎么渲染不同菜单?

企业级项目里,不同角色(管理员”和“普通员工”)能看到的菜单不一样,这时候需要动态过滤菜单数据

场景1:前端静态过滤(简单权限)

如果权限逻辑不复杂,前端可以在渲染前,根据用户角色过滤 menuList,比如用 Vuex 存用户角色,然后过滤出该角色能访问的菜单项。

示例:

<!-- Menu.vue 前端过滤权限 -->
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
import MenuItem from './MenuItem.vue'
const store = useStore()
const userRole = computed(() => store.state.user.role) // 假设存了用户角色
defineProps({
  menuList: {
    type: Array,
    required: true
  }
})
// 过滤出当前角色有权限的菜单
const filteredMenuList = computed(() => {
  return props.menuList.filter(item => {
    // 假设每个菜单项的roles字段是允许访问的角色数组
    return item.roles.includes(userRole.value)
  })
})
</script>
<template>
  <ul class="menu">
    <MenuItem 
      v-for="item in filteredMenuList" 
      :key="item.key" 
      :item="item"
    />
  </ul>
</template>

场景2:后端动态返回(复杂权限)

如果权限逻辑很复杂(比如不同角色的菜单结构完全不同),更安全的方式是后端接口返回当前用户可访问的菜单数据,前端只需要调接口拿到 menuList,直接渲染就行。

示例(伪代码):

<!-- Menu.vue 后端获取菜单 -->
<script setup>
import { ref, onMounted } from 'vue'
import { getMenuList } from '@/api/menu' // 假设接口请求
import MenuItem from './MenuItem.vue'
const menuList = ref([])
onMounted(async () => {
  const res = await getMenuList() // 调接口拿权限内的菜单
  menuList.value = res.data
})
</script>

怎么选? 简单权限用前端过滤(开发快),复杂权限用后端返回(安全且灵活),核心思路都是“只渲染用户有权限的菜单项”。

自定义拓展:怎么让菜单支持自定义内容和主题?

每个项目的UI风格、菜单需求都不一样,得让菜单组件支持和主题切换

用Slot插槽

比如想给菜单项加图标、小红点(Badge),可以用 Vue 的 <slot> 让用户自由插入内容。

示例(给菜单项加前缀图标):

<!-- MenuItem.vue 定义插槽 -->
<template>
  <li class="menu-item">
    <!-- 前缀插槽:用户可自定义图标 -->
    <slot name="prefix" :item="item" />
    <span>{{ item.label }}</span>
    <!-- 后缀插槽:比如加Badge -->
    <slot name="suffix" :item="item" />
  </li>
</template>
<!-- 父组件使用时插入图标 -->
<Menu :menuList="menuList">
  <template #prefix="{ item }">
    <Icon :name="item.icon" /> <!-- 假设Icon是图标组件 -->
  </template>
</Menu>

这样用户想加图标、提示、按钮都能自己插,组件灵活性直接拉满。

主题切换:用CSS变量或动态Class

不同项目的菜单颜色、hover效果不一样,用 CSS变量 能轻松实现主题切换。

示例:

<!-- Menu.vue 定义CSS变量 -->
<style scoped>
.menu {
  --menu-bg: #fff; /* 菜单背景色 */
  --menu-hover-bg: #f5f7fa; /*  hover背景色 */
  --menu-active-bg: #eaf2ff; /* 选中背景色 */
  background-color: var(--menu-bg);
}
.menu-item {
  &:hover { background: var(--menu-hover-bg); }
  &.active { background: var(--menu-active-bg); }
}
</style>

然后在父组件里,通过动态class或内联样式修改这些变量:

<Menu 
  :menuList="menuList"
  class="dark-theme"
/>
<style>
.dark-theme {
  --menu-bg: #1f2d3d;
  --menu-hover-bg: #2b3848;
  --menu-active-bg: #384c63;
}
</style>

这样不用改组件内部样式,换主题只需要加个class,维护成本极低。

性能优化:菜单数据量大时怎么避免卡顿?

如果菜单数据有几百条甚至更多,直接循环渲染会导致DOM节点爆炸,页面卡顿,这时候得做性能优化

方案1:虚拟滚动(适合长列表菜单)

虚拟滚动的核心是“只渲染可视区域的内容”,其他区域用空白占位,可以用 vue-virtual-scroller 这类库实现。

示例(简化逻辑):

<template>
  <!-- 用RecycleScroller实现虚拟滚动 -->
  <RecycleScroller
    :items="menuList"
    :item-size="36" <!-- 每个菜单项高度 -->
    class="virtual-menu"
  >
    <template #default="{ item }">
      <MenuItem :item="item" />
    </template>
  </RecycleScroller>
</template>
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import MenuItem from './MenuItem.vue'
</script>

虚拟滚动能把DOM节点数量从几百个降到几十个,渲染性能飞跃提升。

方案2:优化v-for的Key

如果菜单数据是静态的(或更新不频繁),给 v-forkey 用唯一且稳定的值(比如后端返回的id),避免Vue频繁销毁/创建DOM,减少性能损耗。

打造万能菜单的核心思路

Vue3 做菜单组件,核心是数据驱动 + 组件化 + 场景拓展

  • 基础结构:用 props 传数据,v-for 渲染,解耦结构和数据。
  • 多级嵌套:递归组件处理无限层级,搭配Transition做动画。
  • 生态联动:和Vue Router、Vuex深度整合,实现路由跳转、权限控制。
  • 灵活拓展:Slot插槽自定义内容,CSS变量实现主题切换。
  • 性能兜底:虚拟滚动、Key优化应对大数据量场景。

不管是简单的导航栏,还是复杂的后台多级菜单,掌握这些思路后,就能根据项目需求灵活调整,下次开发菜单时,再也不用从头撸代码啦~

(注:文中代码为简化示例,实际项目需结合错误处理、类型定义等细节完善~)

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门