说在前面
项目使用过程中,用户展开目录树,然后对目录树实施一些增删改查操作,此时如果选择重新获取最新的目录树,整个目录树就会重新刷新,无法记住之前展开的节点信息,十分影响用户体验,本文旨在通过一定技巧,在不刷新整棵目录树的前提下,实现对目录树节点的增删改等操作。
项目实施过程中,一些目录树由于挂载了大量的业务信息,每次获取目录树全部节点信息数据量达到10MB+
,严重影响了用户使用体验,经过讨论,决定使用懒加载的方式,首次获取第一层级的节点,然后根据选中的节点id
获取该节点的子节点信息。
注:以下示例以el-table
为例,el-tree
同理。此示例规定目录树至多展开至4级,1-3级节点前默认节点具有子节点,查询后更新实际数据,4级节点默认不具备展开能力,首层节点的parentId
为 0,level
,parentId
等字段信息为服务端返回。
技术分析
首先这样对于服务端来说,只需要提供一个接口,根据id
获取子节点信息,id
为0
返回第一层级节点信息。
前端方面,官方提供了简单的demo,每次点击节点会自动触发钩子函数。
截止到目前,我们可以基于现有的知识完成目录树的加载功能了,但在实际业务中可能会涉及到不止于增、删、改的业务功能,我们需要在实现该功能的基础上,不刷新全局页面,保证用户体验。
有几个需要注意的点:
- 当使用resolve()方法给某个节点赋值时,由于某些未知原因无法通过resolve([])将节点置空,需要通过下面refresh()中的方法进行置空;
- 懒加载机制,重新调用
resolve()
或者重置treeData
时,之前展开的节点(重新获取后的数据id不变)不会受到影响,不会刷新; - 如果节点想正常展开,需要在恰当的时候赋值
hasChildren
属性; - 在获取数据后,通过level控制展开的最大层级;
- resolve()方法为每个节点独有,通过map的数据结构将方法与id进行绑定。
以下是实现的基本思路。
<template>
<el-table
:data="tableData"
style="width: 100%"
row-key="id"
ref="tableRef"
border
lazy
:load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="date" label="日期" width="180"> </el-table-column>
<el-table-column prop="name" label="姓名" width="180"> </el-table-column>
<el-table-column prop="address" label="地址"> </el-table-column>
</el-table>
</template>
<script>
data() {
return {
this.parentId: '0',
load: false,
treeData: [],
maps: new Map()
}
},
mounted() {
this.getData();
},
methods: {
// 展示首层节点时调用
getData() {
this.load = true; // 开始加载动画
getDataListById('0').then(rst => { // 网络方法,获取首层节点
if (rst.status) { // 标识成功网络访问字段,根据实际情况调整
this.loading = false; // 停止加载动画
rst.records.map((record, i) =>{ // 遍历首层节点
rst.records[i].hasChildren = true; // 给首层节点添加展开按钮,如不赋值该字段,则无展开按钮,无法进行后续操作
})
this.treeData = res.data.records; // 赋值,展示首层节点
}
})
},
// 展开节点时调用
load (tree, treeNode, resolve) {
this.maps.set(tree.id, {tree, treeNode, resolve}) // 点击某个节点后,记录该节点的resolve方法,后续刷新子节点时调用
getDataListById(tree.id).then(rst => {
if (res.status) {
if (tree.level !== 3) { // 当节点不是第四层时,添加展开按钮,level字段服务端提供,此操作可保证目录树展开至多4级
rst.records.map((record, i) =>{
rst.records[i].hasChildren = true;
})
}
resolve(rst.records);
}
})
},
// 当执行增,删,改等操作后,刷新节点用
refresh() {
if (this.parentId === '0') {
this.getData();
return
}
if (!this.maps.get(this.parentId)) { // 如果之前未针对此节点展开,则无需获取该节点下子节点数据
return
}
const { resolve } = this.maps.get(this.parentId); // 之前展开过该节点,根据id获取resolve方法
let that = this // 保存此位置的this指向
getDataListById(this.parentId).then(rst => {
if (res.status) {
if (res.data.records.length === 0) { // 注意,当返回值为空数组(比如上一步操作是删除最后一个节点)需要单独使用此方法处理,使用resolve([]) 方法,无法将节点置空
this.$set(
this.$refs.table.store.states.lazyTreeNodeMap,
that.parentId,
[]
);
return
}
if (rst.records[0].level !== 4) { // 注意此时level要是4,因为获取子节点的id传的是parentId
rst.records.map((record, i) =>{
rst.records[i].hasChildren = true;
})
}
resolve(rst.records);
}
})
}
}
</script>
查询
首次
在mounted()周期函数中获取数据,考虑到后面此方法可能调用多次,封装为getData()
。
点击节点展开
这里在展开的回调方法load()
中储存一下相关的resolve()
方法,后面刷新节点时使用。
删除
deleteNode (data) { // data为目录树行内数据
this.parentId = data.parentId;
// del..
this.refresh()
}
删除分为2种情况,删除首层节点,调用getData()
方法,删除其他节点,调用refresh()
方法,但是如果删除首层节点,此时parentId
为0,由于refresh()
方法的判断,所以此处不论删除哪个节点,都调用refresh()
即可。
编辑
editNode (data) {
this.parentId = data.parentId
// edit..
this.refresh()
}
同理删除,此时调用refresh()即可。
新增
addNode (data) {
if (data === null) { // 新增根节点
this.parentId = '0'
} else { // 新增子节点
this.parentId = data.id
}
// add..
this.refresh()
}
说到最后
以上。