Bootstrap

el-tree 数据改变后局部刷新目录树

说在前面

项目使用过程中,用户展开目录树,然后对目录树实施一些增删改查操作,此时如果选择重新获取最新的目录树,整个目录树就会重新刷新,无法记住之前展开的节点信息,十分影响用户体验,本文旨在通过一定技巧,在不刷新整棵目录树的前提下,实现对目录树节点的增删改等操作。
项目实施过程中,一些目录树由于挂载了大量的业务信息,每次获取目录树全部节点信息数据量达到10MB+,严重影响了用户使用体验,经过讨论,决定使用懒加载的方式,首次获取第一层级的节点,然后根据选中的节点id获取该节点的子节点信息。

注:以下示例以el-table为例,el-tree同理。此示例规定目录树至多展开至4级,1-3级节点前默认节点具有子节点,查询后更新实际数据,4级节点默认不具备展开能力,首层节点的parentId为 0,level,parentId等字段信息为服务端返回。

技术分析

首先这样对于服务端来说,只需要提供一个接口,根据id获取子节点信息,id0返回第一层级节点信息。

前端方面,官方提供了简单的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()
}

说到最后

以上。

;