本文章写于一年前,修改其中的一些图片代码,转载至此,略作订正
写在前面
首先,本篇文章所开发的组件并非一个已经开源的上线组件,所以如果你急于需要一个插件来只做你的项目,那么并不能带给你及时的帮助。这个组件的开发预计写两篇文章,一遍写组件,一篇写组件逻辑。这篇文章也是我自己开发的从无到有的过程,所以它可以为你提供一些Tree组件开发的思路,代码写到一定程度,不能完全依赖插件了,有时间可以看看插件源码或者动手去开发,这样真的能加深对技术的掌握程度。
开发过程
1.数据仓库-Vuex
2.组件的循环创建-递归组件
需求决定了我的技术选型,项目需求是一个中国各级政府列表的选择,后台基于大数据去汇总各级政府网站的文章决策,然后交给前端去展示,左侧显示各级政府的Tree,右侧显示具体文章。因为政府数据的不确定性,所以显示政府的Tree的每一层级也是动态获取的,就比如北京市下级的海淀区政府在一小时前可能不存在某某政策文章,那么北京市节点的子节点中就不会包含海淀区这一子节点,但是10分钟前网站发布了一篇文章,那么就需要在北京市中添加海淀区,为了良好的实时性效果,每一级的子节点都不固定。
因此树状结构的data是变化的而不是在初始化的时候就固定好的,数据驱动Dom的思想,data有多少层级,Dom就有多少层级,data不定,所以Dom也无法提前定义好层级,决定采用Vue的递归组件,组件递归自身实现无限制层级的渲染。需求中每一个节点的Tree都具有增删改的功能,因为data是单例的,全局维护着一个data数据源头,增删改也就是操作data第n个子节点的某个子元素,因此采用了Vuex作为一个data存放的仓库(这不是一个组件库,只是一个项目的应用)。
项目的关键点:
1.动态加载子节点,也就是说页面加载的时候,根节点的nodes子节点数组可能是个空数组,每次单击节点去获取子节点数组。
2.全局单例的树节点对象 data 对象,每次得到新的子节点,需要去维护这个对象,因为数据与dom动态绑定,所以每次数据对象内部发生了变化,dom也会重绘。
vue对虚拟dom的映射通过使用diff算法进行了优化,所以不用担心,重绘造成页面闪屏
树data数据结构如下:
let data = {
id: "01",
lable: "政府机构",
nodes: [
{
id: "02",
lable: "中央机关",
nodes: [{
//..
}]
},
{
id: "03",
lable: "直辖市",
nodes: [{
//..
//北京市...
//天津市..
}]
},
]
};
复制代码
显示成这样:
每一个节点包含id,label,nodes三个属性,nodes往下延伸子节点,一共有多少级不确定根据后台获取得到。
递归组件
一个简单的递归组件的示例如下:
<template>
<div class="tree-menu">
//组件内部不断用自身,只要子节点存在就递归调用
<tree-menu v-for='(item, index) of dataNodes' :key='index'></tree-menu>
</div>
</template>
<script>
export default {
name:'TreeMenu',//组件名称必须写
data() {
return {
dataNodes: {
//...
}
}
}
}
</script>
复制代码
TreeMenu.vue声明组件TreeMenu,并向外暴露,在组件内部调用自身,也就是一个递归的思想,绑定的dataNodes有多少层级,那就会递归多少层,因为每一层都有v-for都会循环子节点。绑定具体数据的时候再具体分析。不断调用自身,当然这只是一个例子,实际情况还要改造一下:
新建TreeMenu.vue,作为显示节点的逻辑组件:
<template>
<div class="tree-menu">
<div>{{label}}</div>//节点名称
<tree-menu //如果nodes.length>0就递归显示子节点
v-for="(node, index) of nodes"
:key="index"
:nodes="node.nodes" //子节点的子节点向下传递
:label="node.label"
>
</tree-menu>
</div>
</template>
<script>
export default {
name: "TreeMenu",
props:['label','nodes'],//数据通过pros向下传递,全局唯一数据源
data() {
return {};
}
};
</script>
复制代码
创建Tree.vue作为节点树的入口:
<template>
<div class="tree-alone">
<tree-menu :label="tree.label" :nodes="tree.nodes"></tree-menu>
</div>
</template>
<script>
import TreeMenu from './TreeMenu.vue'
export default {
name: 'tree',
data() {
return {
tree: {
id: "01",
lable: "总层级",
nodes: [
{
id: "02",
label: "层级1",
nodes: [{
label: '层级1-1'
}]
},
{
id: "03",
label: "层级2",
nodes: []
},
]
};
}
}
},
components: {
TreeMenu
}
}
</script>
复制代码
在Tree.vue入口组件里引入子组件TreeMenu.vue,并且向子组件传递数据label和nodes,这里data先预定义一个简单的对象,后面会动态获取。
TreeMenu.vue作为树形菜单组件,负责递归和数据渲染,它会接受来自入口组件传递来的label和nodes数据,进行渲染,并且继续递归传递nodes和label。
在从父组件接收nodes和label后,先循环渲染n个tree-menu组件,然后每一个tree-menu又会递归自身,所以数据就这样一层层向下传递nodes=>nodes.node=>nodes.node.node,此时已经完成了数据与Dom的绑定,可以修改一下Tree.vue中的tree数据源,看一下组件是否动态改变了。现在渲染如下图:
数据正确渲染,现在需要加一些样式和点击事件(展开与收缩),就不做详细介绍了,代码如下: (向下传递depth参数来获取层级索引)
//Tree.vue
<template>
<div class="tree-alone">
<tree-menu :label="tree.label" :nodes="tree.nodes" :depth="0"></tree-menu>
</div>
</template>
复制代码
//TreeMenu.vue
<template>
<div class="tree-menu">
<div :style="indent" @click="toggleChildren">{{label}}</div>
<div v-if="showChildren">
<tree-menu
v-for="(item, index) of nodes"
:key="index"
:nodes="node.nodes"
:label="node.label"
:depth="depth + 1"
></tree-menu>
</div>
</div>
</template>
<script>
export default {
name: "TreeMenu",
props: ["label", "nodes"],
data() {
return {
showChildren: false
};
},
methods: {
toggleChildren() {
this.showChildren = !this.showChildren;
}
},
computed: {
indent() {
return { transform: `translate(${this.depth * 20}px)` };
}
}
};
</script>
复制代码
修改后会展开收缩与缩进,一些箭头旋转等样式问题就不详细写了。