Vue3 嵌套导航最稳的做法,是用递归组件把同一份树形菜单数据渲染成任意层级,而不是为每一层都写一套死模板。在后台管理系统里,经常遇到「主导航里还要再嵌一层子导航」的需求:左边点开「设置」,右边再出现「账户 / 安全 / 通知」这样的二级导航。本文基于 Vue 3 组合式 API,演示如何用一份数据驱动多级导航,并提醒 3 个最容易翻车的点。Vue3 嵌套导航的本质是「树」,处理树最自然的方式就是递归,只要菜单数据本身是嵌套结构,写一个会调用自身的 NavItem 组件就能渲染任意深度。新增层级时你不用改模板,真正要小心的只有 key、选中态和移动端折叠这三处。这套 Vue3 嵌套导航写法可以直接搬进后台项目,后续无论菜单加几层都不用再动代码。

先看结论
Vue3 嵌套导航用递归组件最省心:一份树形数据、一个会调用自己的 NavItem,就能渲染任意层级的子导航。三种常见方案里,递归组件胜在「层级不固定也能通吃」,v-for 平铺只适合固定两层,UI 库则牺牲定制换省事。把「层数」这个变量从模板里消除,是这套写法的核心价值。记住三个坑——用稳定 key、选中态按路由命中后代、移动端用点击切换本地展开状态。
一、最小场景与菜单数据结构
先定义一份典型的树形菜单,每个节点带 label、path 和可选的 children:
[
{ "label": "仪表盘", "path": "/dashboard" },
{
"label": "设置",
"path": "/settings",
"children": [
{ "label": "账户", "path": "/settings/account" },
{ "label": "安全", "path": "/settings/security" },
{
"label": "通知",
"path": "/settings/notify",
"children": [
{ "label": "邮件", "path": "/settings/notify/email" },
{ "label": "短信", "path": "/settings/notify/sms" }
]
}
]
}
]
这份结构里「通知」下面还有「邮件 / 短信」,就是导航栏里再嵌子导航的典型情况。子导航的层级完全由数据决定,组件本身不需要知道有几层。数据驱动的好处是:后台改菜单配置,前端不用发版。先过一遍 Vue3 教程 的组件章节,理解 props 与递归调用,再回来读下面的实现会更顺。
配置阶段要确认后端返回的菜单是树还是扁平列表。如果是扁平列表(带 parentId),需要先在前端转成树再交给递归组件,转换逻辑只写一次,渲染层保持不变。这个前置步骤做错,后面递归组件拿到的就不是嵌套结构,渲染会直接失败。做 Vue3 嵌套导航时,建议先用一份本地 mock 数据把结构跑通,再接真实接口,排错成本最低。路由方面,先确认是否已经安装并注册 Vue Router,没有的话要先把路由表建好,否则子导航跳转会找不到页面。

二、用递归组件渲染多级导航
新建 NavItem.vue,它在渲染自己的子节点时再次引用自己:
<script setup>
defineProps({
item: { type: Object, required: true }
})
</script>
<template>
<ul class="nav">
<li>
<router-link :to="item.path">{{ item.label }}</router-link>
<NavItem
v-if="item.children && item.children.length"
v-for="child in item.children"
:key="child.path"
:item="child"
/>
</li>
</ul>
</template>
NavItem 在 <script setup> 中可以直接通过文件名自我引用,无需额外注册。父组件只要把整份菜单作为 :item 传入根节点即可。这个会调用自身的导航栏组件,核心价值就是把「层数」从模板里消除。递归组件的边界是:必须有「递归出口」——当节点没有 children 时就不再渲染子 NavItem,否则会无限递归直到栈溢出。生产环境建议给递归深度加一个上限(例如 5 层),避免后端返回了异常深的环数据导致崩溃。
Vue Router 在这里负责子导航的跳转与高亮。router-link 会把当前匹配的路由自动加上 router-link-active 类,配合 CSS 就能做出「当前子导航高亮」的效果。如果不用 Vue Router,纯靠手动维护 class 也能实现,但路由驱动的方案更适合后台系统,因为刷新页面后高亮状态不会丢。Vue3 嵌套导航配合路由时,路由表本身就是一份天然的菜单数据,可以一源多用。
三、三种嵌套方案的取舍
| 方案 | 适用场景 | 优点 | 代价 |
|---|---|---|---|
| 递归组件 | 层级不固定、菜单来自后端 | 一套模板通吃任意深度 | 需要理解自我引用 |
| v-for 平铺 | 固定两层 | 写法直观、好调试 | 加第三层就要改模板 |
| UI 库(如 Element Plus 的 Menu) | 想省事、要现成交互 | 自带展开收起、键盘导航 | 样式与行为被框架锁定 |
递归组件胜在「未来加层级零成本」,代价是初学者第一次看到组件调用自己会懵;平铺写法适合确认只有两层的简单后台;UI 库适合交付压力大、不追求定制的团队。一个会调用自身的导航栏组件,核心价值就是把「层数」这个变量从模板里消除。三种方案的对比告诉我们:选型看的是「层级会不会变」,而不是「现在有几层」。性能上,递归渲染几十个节点几乎无感,真正要防的是异常深的环数据拖垮渲染。想把这套导航写得更扎实,可以对照 Vue3 课程 里的组件与路由章节系统过一遍,把递归组件和 Vue Router 的配合练熟,再回看前面的取舍会更清晰。

四、3 个最容易踩的坑
4.1 数据缺少稳定 key 导致渲染错乱
v-for 必须用唯一且稳定的 key。很多人图省事用数组下标,结果展开收起时 Vue 复用了错误的 DOM,子导航显示错位。正确做法是用 path 或后端给的 id 作为 key,如上例中的 :key="child.path"。数组下标在「只展开不增删」时碰巧不出错,一旦动态增删子项,Vue 的复用策略就会错位,属于迟早会爆的隐患。这个失败的排查点就在 key,而不是组件逻辑,修复时优先检查每一项是不是用了稳定标识。
4.2 选中态只记录最后一级
常见的错误是把「当前路由」直接塞进一个全局变量,切换子导航时父级高亮丢失。正确做法是每个 NavItem 都结合 Vue Router 的 route.path 判断是否命中:命中任一后代就给父级加高亮类,这样「设置」在访问「设置 / 安全」时依然亮着。这个性能取舍很划算——每次路由变化只做一次前缀匹配,开销可以忽略。适用边界是:扁平菜单用前缀匹配即可,超大规模菜单可加缓存避免重复计算。
4.3 移动端折叠后子导航不展开
桌面端鼠标悬停展开,移动端没有 hover,必须用点击切换一个 open 状态。递归组件里要把 open 作为局部 ref 放在每个节点上,父节点不代管子节点的展开,否则点开 A 会把 B 也带开。适用边界是:移动端用 click 触发,桌面端用 hover 触发,两者可以共存,但状态要各自独立,不要共享同一个 open。验证方法是分别在窄屏和宽屏下点开不同子项,确认互不干扰、收起状态正确。
总结
多级导航用递归组件最省心:一份树形数据、一个会调用自己的 NavItem,就能渲染任意层级的子导航。Vue3 嵌套导航的本质是树形数据驱动,记住三个坑——用稳定 key、选中态按路由命中后代、移动端用点击切换本地展开状态。把这套结构放进后台项目,后续加菜单层级都不用再动模板。
要点带走:
- 菜单数据用嵌套结构,递归渲染最自然;
- key 用 path 或 id,别用数组下标;
- 选中态按 route.path 命中,父级才能保持高亮。
下一步可以结合路由守卫,给未登录用户自动收起受限菜单。
常见问题
Q:递归组件会不会有性能问题?
层级在十层以内、节点几百个时几乎没有感知差别;真正要防的是后端返回了含环的异常数据,所以加一个最大深度上限最稳妥。
Q:后端给的菜单不是树,是扁平列表怎么办?
先在前端按 parentId 转成树再交给递归组件,转换逻辑只写一次,渲染层保持不变。
Q:为什么我用数组下标做 key 看起来也正常?
下标在「只展开不增删」时碰巧不出错,一旦动态增删子项,Vue 的复用策略就会错位,属于迟早会爆的隐患。
延伸学习
想把 Vue 3 多级菜单这块补扎实,可以按这个顺序来:
- 路由与菜单联动先看 Vue Router:如何实现页面路由,理解 route.path 与组件渲染的关系;
- 系统过一遍 Vue 3.0 新特性全面解析,把组合式 API 用熟;
- 想动手做完整后台,小白学前端:Vue.js实现电商后台管理系统(企业项目实战) 是边学边写的实战形式。

TRAE-AI编程



