前言
小伙伴们好久不见,最近刚入职新公司,需求排的很满,平常是实在没时间写文章了,更新频率会变得比较慢。
周末在家闲着无聊,突然小弟过来紧急求助,说是面试腾讯的时候,对方给了个 Vue 的递归菜单要求实现,回来找我复盘。
正好这周是小周,没想着出去玩,就在家写写代码吧,我看了一下需求,确实是比较复杂,需要利用好递归组件,正好趁着这个机会总结一篇 Vue3 + TS 实现递归组件的文章。
需求
可以先在 Github Pages[1] 中预览一下效果。
需求是这样的,后端会返回一串可能有无限层级的菜单,格式如下:
[
{
id: 1,
father_id: 0,
status: 1,
name: '生命科学竞赛',
_child: [
{
id: 2,
father_id: 1,
status: 1,
name: '野外实习类',
_child: [{ id: 3, father_id: 2, status: 1, name: '植物学' }],
},
{
id: 7,
father_id: 1,
status: 1,
name: '科学研究类',
_child: [
{ id: 8, father_id: 7, status: 1, name: '植物学与植物生理学' },
{ id: 9, father_id: 7, status: 1, name: '动物学与动物生理学' },
{ id: 10, father_id: 7, status: 1, name: '微生物学' },
{ id: 11, father_id: 7, status: 1, name: '生态学' },
],
},
{ id: 71, father_id: 1, status: 1, name: '添加' },
],
},
{
id: 56,
father_id: 0,
status: 1,
name: '考研相关',
_child: [
{ id: 57, father_id: 56, status: 1, name: '政治' },
{ id: 58, father_id: 56, status: 1, name: '外国语' },
],
},
]
- 每一层的菜单元素如果有
_child
属性, 这一项菜单被选中以后就要继续展示这一项的所有子菜单,预览一下动图:
- 并且点击其中的任意一个层级,都需要把菜单的 完整的
id
链路 传递到最外层,给父组件请求数据用。比如点击了科学研究类
。那么向外emit
的时候还需要带上它的第一个子菜单植物学与植物生理学
的id
,以及它的父级菜单生命科学竞赛
的 id,也就是[1, 7, 8]
。 - 每一层的样式还可以自己定制。
实现
这很显然是一个递归组件的需求,在设计递归组件的时候,我们要先想清楚数据到视图的映射。
在后端返回的数据中,数组的每一层可以分别对应一个菜单项,那么数组的层则就对应视图中的一行,当前这层的菜单中,被点击选中 的那一项菜单的 child
就会被作为子菜单数据,交给递归的 NestMenu
组件,直到某一层的高亮菜单不再有 child
,则递归终止。
由于需求要求每一层的样式可能是不同的,所以再每次调用递归组件的时候,我们都需要从父组件的 props
中拿到一个 depth
代表层级,并且把这个 depth + 1
继续传递给递归的 NestMenu
组件。
重点主要就是这些,接下来编码实现。
先看 NestMenu
组件的 template
部分的大致结构:
<template>
<div class="wrap">
<div class="menu-wrap">
<div
class="menu-item"
v-for="menuItem in data"
>{
{menuItem.name}}</div>
</div>
<nest-menu
:key="activeId"
:data="subMenu"
:depth="depth + 1"
></nest-menu>
</div>
</template>
和我们预想设计中的一样, menu-wrap
代表当前菜单层, nest-menu
则就是组件本身,它负责递归的渲染子组件。
首次渲染
在第一次获取到整个菜单的数据的时候,我们需要先把每层菜单的选中项默认设置为第一个子菜单,由于它很可能是异步获取的,所以我们最好是 watch
这个数据来做这个操作。
// 菜单数据源发生变化的时候 默认选中当前层级的第一项
const activeId = ref<number | null>(null)
watch(
() => props.data,
(newData) => {
if (!activeId.value) {
if (newData && newData.length) {
activeId.