前言

本文作者是华为Vue DevUI负责人Kagol,感谢你的无私分享!

配套视频

下面是本期直播视频,欢迎观看学习,不要忘了三连+分享哦!

我要做开源第二期:华为大佬亲授,做个像样的Tree组件_二级

回顾

上一期直播我们给大家分享了如何给Vue DevUI开源组件库提交第一个PR,并以tree组件为例子,介绍如何给Vue DevUI贡献组件,上次只开了个头,写了一个渲染一层树节点的非常简单的tree组件,并且只有data这一个api。

我要做开源第二期:华为大佬亲授,做个像样的Tree组件_嵌套_02

本次我们将更进一步,实现一个可以有层级嵌套,并且有缩进控制和折叠图标的树:

我要做开源第二期:华为大佬亲授,做个像样的Tree组件_嵌套_03

整体设计思路

实现一个tree组件,我们的第一直觉就是一层一层嵌套渲染子节点的dom结构,这么做会有一个问题,就是如果一棵树有非常多节点,并且嵌套层级非常深,我们很难用虚拟滚动的方式去进行优化,因此不可避免地导致性能问题。

我测试了ElementPlus组件库,使用Tree组件渲染5万个树节点,耗时6s左右,同样的数据量,AntDesign组件库的Tree组件耗时10s左右,但是AntDesign的Tree组件提供了虚拟滚动功能,开启虚拟滚动,加载时间瞬间降到1s以内,而且不会因为节点数的增加而影响性能。

而要使用虚拟滚动,就需要将嵌套结构变成平铺结构。

为了方便用户使用,我们设计的data属性依然使用嵌套结构,但是组件内部需要将其拍平,并用平铺的方式将树节点渲染到dom中。

data结构:

data: [
{
label: 'node-1',
children: [
{
label: 'node-11',
children: [
{ label: 'node-111' },
{ label: 'node-112' },
],
},
{
label: 'node-12',
children: [
{ label: 'node-121' },
{ label: 'node-122' },
{ label: 'node-123' },
],
},
],
},
{
label: 'node-2'
},
]

DOM结构:

<div class="node-1">
<div class="node-11">
<div class="node-111"></div>
<div class="node-112"></div>
</div>
<div class="node-12">
<div class="node-121"></div>
<div class="node-122"></div>
<div class="node-123"></div>
</div>
</div>
<div class="node-2"></div>

->

<div class="node-1"></div>
<div class="node-11"></div>
<div class="node-111"></div>
<div class="node-112"></div>
<div class="node-12"></div>
<div class="node-121"></div>
<div class="node-122"></div>
<div class="node-123"></div>
<div class="node-2"></div>

引入SVG

由于tree组件节点前面一般会有一个小图标,为了方便使用svg图标,我们可以借助vite-svg-plugin插件。

安装vite-svg-loader插件

yarn add -D vite-svg-loader

docs/vite.config.ts

import path from 'path'
import { defineConfig } from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
import svgLoader from 'vite-svg-loader' // 引入vite-svg-loader插件

export default defineConfig({
resolve: {
alias: [
{ find: '@devui', replacement: path.resolve(__dirname, '../devui') },
]
},
plugins: [
vueJsx({}),
svgLoader(), // 使用vite-svg-loader插件
],
})

导入svg

import IconOpen from './assets/open.svg'

open.svg

<svg
width="16px"
height="16px"
viewBox="0 0 16 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="svg-icon svg-icon-close"
>
<g stroke-width="1" fill="none" fill-rule="evenodd">
<rect x="0.5" y="0.5" width="15" height="15" rx="2" stroke="#5e7ce0"></rect>
<rect x="4" y="7" width="8" height="2" fill="#5e7ce0"></rect>
</g>
</svg>

使用svg

<template>
<IconOpen />
</template>

or

setup() {
return () => <IconOpen />
}

给节点增加一个层级的标识level

不同层级节点的缩进是不一样的,我们需要一个level属性来标识当前节点的层级。

第一层的level是1,第二层是2,以此类推。

data: [
{
label: 'node-1',
level: 1,
children: [
{
label: 'node-11',
level: 2,
children: [
{ label: 'node-111', level: 3, },
{ label: 'node-112', level: 3, },
],
},
{
label: 'node-12',
level: 2,
children: [
{ label: 'node-121', level: 3, },
{ label: 'node-122', level: 3, },
{ label: 'node-123', level: 3, },
],
},
],
},
{
label: 'node-2',
level: 1,
},
]

渲染多层树节点(重点)

渲染一层节点非常简单:

<div class="devui-tree">
{ props.data.map(item <div>{item.label}</div>) }
</div>

渲染多层则需要定义一个渲染函数,在函数中做一次递归操作。

// 增加缩进的展位元素
const Indent = () {
return <span style="display: inline-block; width: 16px; height: 16px;"></span>
}

const renderNode = (item) => {
return (
<div class="devui-tree-node" style={{ paddingLeft: `${24 * (item.level - 1)}px` }}>
{ !item.children ? <IconOpen class="mr-xs" /> : <Indent /> }
{ item.label }
</div>
)
}

const renderTree = (tree) => {
return tree.map(item {
if (!item.children) {
return renderNode(item)
} else {
return (
<>
{renderNode(item)}
{renderTree(item.children)}
</>
)
}
})
}
<div class="devui-tree">
{ renderTree(props.data) }
</div>

demo文档

# Tree 树

一种表现嵌套结构的组件。

### 何时使用

文件夹、组织架构、生物分类、国家地区等等,世间万物的大多数结构都是树形结构。使用树控件可以完整展现其中的层级关系,并具有展开收起选择等交互功能。

### 基础用法

<d-tree :data="data"></d-tree>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup() {
const data = ref([{
label: '一级 1', level: 1,
children: [{
label: '二级 1-1', level: 2,
children: [{
label: '三级 1-1-1', level: 3,
}]
}]
}, {
label: '一级 2', level: 1,
children: [{
label: '二级 2-1', level: 2,
children: [{
label: '三级 2-1-1', level: 3,
}]
}, {
label: '二级 2-2', level: 2,
children: [{
label: '三级 2-2-1', level: 3,
}]
}]
}, {
label: '一级 3', level: 1,
children: [{
label: '二级 3-1', level: 2,
children: [{
label: '三级 3-1-1', level: 3,
}]
}, {
label: '二级 3-2', level: 2,
children: [{
label: '三级 3-2-1', level: 3,
}]
}]
}, {
label: '一级 4', level: 1,
}])

return {
data,
}
}
})
</script>

```vue
<template>
<d-tree :data="data"></d-tree>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup() {
const data = ref([{
label: '一级 1', level: 1,
children: [{
label: '二级 1-1', level: 2,
children: [{
label: '三级 1-1-1', level: 3,
}]
}]
}, {
label: '一级 2', level: 1,
children: [{
label: '二级 2-1', level: 2,
children: [{
label: '三级 2-1-1', level: 3,
}]
}, {
label: '二级 2-2', level: 2,
children: [{
label: '三级 2-2-1', level: 3,
}]
}]
}, {
label: '一级 3', level: 1,
children: [{
label: '二级 3-1', level: 2,
children: [{
label: '三级 3-1-1', level: 3,
}]
}, {
label: '二级 3-2', level: 2,
children: [{
label: '三级 3-2-1', level: 3,
}]
}]
}, {
label: '一级 4', level: 1,
}])

return {
data,
}
}
})
</script>
```

### Props

| 参数 | 类型 | 默认 | 说明 | 跳转 Demo |
| ------------ | ------- | ----- | ---------------------------------------- | --------- |
| data | `TreeData` | `[]` | 必选,数据源 | |

### TreeData 数据结构

| 参数 | 类型 | 默认值 | 说明 |
| ----------- | --------- | ------- | ------------------------------------------------------------------------ |
| label | `string` | `-` | 文本内容 |
| level | `number` | `-` | 节点层级 |
| children | `TreeData` | `-` | 子节点 |