论基于el-tree如何实现分页加载数据

1,963 阅读3分钟

实现效果如图:

2022-11-17 20.41.45.gif

前言

假如我们的树形图含有大量的数据,一次全部加载出来会很慢,用户体验不好,我们要做数据的分级加载,也就是懒加载,但如果每级的数据量也很大,就需要每一级都做分页加载了。

思路

思路二是思路一的优化版,为可以分开阅读,两版内容部分文字会一致

思路一:在节点下方添加表示点击加载更多的icon

效果如图:

image.png

  • 使用el-tree组件的懒加载lazy,及加载子树数据的方法load
  • 由于是树形结构,所以不适合使用el-pagination这个分页器组件,所以需要在每一页数据的最后一条下方加一个表示加载更多的图标,如果当前数据小于设定的每页数据返回数limit则不需要加这个图标,
  • 所以每个node节点需要加一个变量:isShow用于控制箭头⬇️的展示
//...以上省略了代码,主要是请求树形结构数据
let treeData = res.data || [];
treeData.forEach((item, index) => {
// 控制点击加载更多按钮是否显示
  if (index == treeData.length - 1 && treeData.length == 20) {
    item.isShow = true;
  }
  ...
});
  • 点击加载更多时,传入的参数需要根据后端要求,所以这里主要记录传入参数pagelimitlimit可以限制死,比如20,像page的话,初始化可以为1,后续的数值需要+1,所以需要一个页数映射表pageMap,可以new Map(),简单的Map大家肯定看得懂,粗略代码如下:
handleTreeParams(params) {
    params = {
        appUri: this.$route.query.appUri,
        urlNode: params.urlNode,
        urlPath: params.urlPath,
        page: params.page,
        limit: 20,
    }
    return params
}

pageMap: new Map(),

const urlNode = node.parent.data.urlNode;
const urlPath = node.parent.data.urlPath;
this.pageMap.set(`${urlNode}_${urlPath}`, this.pageMap.has(`${urlNode}_${urlPath}`) ? this.pageMap.get(`${urlNode}_${urlPath}`) + 1 : this.pageCount);

page: this.pageMap.get(`${urlNode}_${urlPath}`)
  • 后一页数据返回后就需要将数据添加到上一页数据的下面了,从elementUI文档中可以知道有三种方式可以加节点:

    • append:为 Tree 中的一个节点追加一个子节点
    • insertBefore:为 Tree 的一个节点的前面增加一个节点
    • insertAfter:为 Tree 的一个节点的后面增加一个节点
  • 这里我们需要用到append,它接受两个参数:(data, parentNode) 接收两个参数,1. 要追加的子节点的data, 2. 子节点的parentdata、key或者node

  • 将下一页请求到的数据使用append添加到节点后面

treeData.forEach((item) => {
  if (node && node.parent) {
     this.$refs.tree.append(item, node.parent.data);
  }
});
  • 需要注意,我们使用的是load方法,它传入两个参数noderesolve,我们在点击加载更多之后并不需要再执行resolve这个方法,如再执行这个方法,会将下一页返回的数据以根节点的形式再次渲染在第一个根节点的下方,如下图所示:

image.png

  • 所以需要在首次渲染完毕之后不再执行resolve,定义一个isGetMore变量,用于控制是否点击加载更多
// 第一次加载
if (!isGetMore) {
  resolve(treeData);
} else {
  treeData.forEach((item) => {
    if (node && node.parent) {
       this.$refs.tree.append(item, node.parent.data);
    }
  });
}

思路二:在节点之后添加一个节点表示加载更多(思路一的优化版)

效果如图:

image.png

  • 使用el-tree组件的懒加载lazy,及加载子树数据的方法load
  • 由于是树形结构,所以不适合使用el-pagination这个分页器组件,所以需要在每一页数据的最后一条节点下加一个加载更多节点,如果当前数据小于设定的每页数据返回数limit则不需要加这个节点,
  • 在每次返回的数据节点后面,添加一个自定义的node节点,这个自定义节点内容如下:
    • apiId、id,需要保证唯一性,因为在每次点击加载更多后,需要将被点击的这个加载更多节点进行删除,以保证树形结构数据的连续性
// 树中添加加载更多节点
    addGetMoreNode(node, treeData) {
    // handleNodePage是保存node页数的一个Map结构
      if (!this.handleNodePage('has', node)) {
        this.handleNodePage('set', node, 1);
      }
      treeData.push({
      // apiId、id需要保证唯一性
        apiId: `${node.data.urlPath}_loadMore_${this.handleNodePage(
          'get',
          node
        )}`,
        id: `${node.data.urlPath}_loadMore_${this.handleNodePage('get', node)}`,
        isLeaf: true,
        urlNode: 2,
        urlCount: '',
        urlPath: node.data.urlPath,
        urlPathList: node.data.urlPathList,
        isGetMore: true
      });
    },
  • isGetMoretrue是必须的,为判断点击的节点是否是加载更多节点,以执行加载更多的方法
// 点击节点
    handleNodeClick(data, Node) {
      this.isAutoMode = false;
      if (Node.isLeaf && !data.isGetMore) {
        this.id = data.apiId;
      }
      if (data.isGetMore) {
      // 执行加载更多的方法
        this.getNodeMore(Node);
      }
    },
  • 点击加载更多时,传入的参数需要根据后端要求,所以这里主要记录传入参数pagelimitlimit可以限制死,比如20,像page的话,初始化可以为1,后续的数值需要+1,所以需要一个页数映射表pageMap,可以new Map(),简单的Map大家肯定看得懂,粗略代码如下:
// 操作页数
    handleNodePage(type, node, val = '') {
      const urlNode = node.parent.data.urlNode;
      const urlPath = node.parent.data.urlPath;
      if (type === 'get') {
        return this.pageMap.get(`${urlNode}_${urlPath}`);
      } else if (type === 'set') {
        return this.pageMap.set(`${urlNode}_${urlPath}`, val);
      } else {
        return this.pageMap.has(`${urlNode}_${urlPath}`);
      }
    },
  • 后一页数据返回后就需要将数据添加到上一页数据的下面了,从elementUI文档中可以知道有三种方式可以加节点:

    • append:为 Tree 中的一个节点追加一个子节点
    • insertBefore:为 Tree 的一个节点的前面增加一个节点
    • insertAfter:为 Tree 的一个节点的后面增加一个节点
  • 这里我们需要用到append,它接受两个参数:(data, parentNode) 接收两个参数,1. 要追加的子节点的data, 2. 子节点的parentdata、key或者node

  • 将下一页请求到的数据使用append添加到节点后面

  • 在后一页数据返回后,我们也得将点击的加载更多节点进行删除,那可以使用remove方法

    • remove:删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性
  • 需要注意,我们使用的是load方法,它传入两个参数noderesolve,我们在点击加载更多之后并不需要再执行resolve这个方法,如再执行这个方法,会将下一页返回的数据以根节点的形式再次渲染在第一个根节点的下方,如下图所示:

image.png

  • 所以需要在首次渲染完毕之后不再执行resolve,定义一个isGetMore变量,用于控制是否点击加载更多
// 获取树形数据
async getTree(params, resolve, node, isGetMore = false) {
  // 省略部分代码...
  if (!isGetMore) {
     resolve(treeData);
  } else {
    treeData.forEach((item) => {
       if (node && node.parent) {
          this.$refs.tree.append(item, node.parent.data);
       }
     });
     node && this.$refs.tree.remove(node.data);
   }
  })
  .catch((err) => {
     this.apiDetail = {};
     this.$message.error(err.msg);
  });
},

实现

qz-pro-virtual-tree是基于el-tree封装的组件,使用el-tree效果也是一样的

思路一主要代码:

文中一些未出现的方法,为实际项目中使用到的方法、参数,使用时替换为自己的即可

<div class="tree-container">
            <qz-pro-virtual-tree
              lazy
              highlight-current
              ref="tree"
              class="tree"
              node-key="apiId"
              icon-class="expand-icon"
              :indent="0"
              :props="props"
              :load="loadNode"
              :default-expanded-keys="defaultExpandedKeys"
              @node-click="handleNodeClick"
            >
              <div class="row-context" slot-scope="{ node, data }">
                <qz-icon
                  class="icon-style icon-folder"
                  v-if="!node.isLeaf"
                ></qz-icon>
                <span
                  class="method-style"
                  v-if="data.urlCount === 0"
                  :class="{
                    red: data.methods && data.methods[0] == 'POST',
                    green: data.methods && data.methods[0] == 'GET'
                  }"
                >
                  【{{ data.methods && data.methods[0] }}】
                </span>
                <el-tooltip
                  class="item"
                  placement="top"
                  v-if="data.urlCount === 0"
                >
                  <div slot="content">
                    <qz-copy btn-class="copy-btn">{{ data.url }}</qz-copy>
                  </div>
                  <span class="url">{{ data.label }}</span>
                </el-tooltip>
                <span v-else>
                  {{ data.label }}
                </span>
                <!-- 去拆分区合并按钮 -->
                <div class="api-choice" v-if="data.urlCount === 0">
                  <span class="api-choice--right">
                    <el-tooltip
                      class="item"
                      effect="dark"
                      content="去拆分"
                      placement="top"
                    >
                      <qz-icon
                        class="icon-split color-main"
                        @click.native="goSplit(data.url)"
                      ></qz-icon>
                    </el-tooltip>
                    <el-tooltip
                      class="item"
                      effect="dark"
                      content="去合并"
                      placement="top"
                    >
                      <qz-icon
                        @click.native="goMerge(data.url)"
                        class="icon-join color-main"
                      ></qz-icon>
                    </el-tooltip>
                  </span>
                </div>
                <!-- 加载更多 -->
                <div class="get-more">
                  <div class="get-more--icon" :ref="data.isShow ? 'more' : ''">
                    <el-tooltip
                      class="item"
                      effect="dark"
                      content="加载更多"
                      placement="top"
                    >
                      <qz-icon
                        @click.native="getNodeMore(node)"
                        class="icon-arrow-down-copy color-main"
                        v-if="data.isShow"
                      ></qz-icon>
                    </el-tooltip>
                  </div>
                </div>
              </div>
            </qz-pro-virtual-tree>
          </div>
pageCount: 2,
pageMap: new Map(),

// 加载节点
    loadNode(node, resolve) {
      getAppUriData(this.$route.query.appUri).then((res) => {
        // 渲染第一级
        if (node.level === 0) {
          this.getTree(
            { urlNode: 0, urlPath: res.data.app.host, page: 1 },
            resolve,
            node
          );
          this.node = node;
          this.resolve = resolve;
        }
        if (node.isLeaf) {
          return resolve([]);
        }
        // 处理其余节点
        if (node.data) {
          this.getTree(
            { urlNode: node.data.urlNode, urlPath: node.data.urlPath, page: 1 },
            resolve
          );
        }
      });
    },
 
// 获取树形数据
    getTree(params, resolve, node, isGetMore = false) {
      Object.assign(this.condition, params);
      let postParams = JSON.parse(JSON.stringify(this.condition));
      postParams = this.handleTreeParams(postParams);
      this.searchParams = deepCopy(postParams);
      getAppStructureTreeData(postParams)
        .then((res) => {
          let activeIndex = undefined;
          let treeData = res.data || [];
          treeData.forEach((item, index) => {
            // 控制点击加载更多按钮是否显示
            if (index == treeData.length - 1 && treeData.length == 20) {
              item.isShow = true;
            }
            item['_locateIndex'] = index;
            item.isLeaf = item.urlCount == 0;
            item.url = formatUri(item.apiUri);
            item.apiId = item.apiId || item.urlPath;
            if (item.isLeaf) {
              item.label = item.urlPath;
            } else {
              item.label = `${item.urlPath}${item.urlCount})`;
            }
            if (treeData.length > 0) {
              activeIndex =
                this.siblingType == 'prev'
                  ? treeData.length - 1
                  : activeIndex || 0;
              let defaultData = treeData[activeIndex];
              if (!defaultData.isLeaf && this.isAutoMode) {
                this.defaultExpandedKeys.push(defaultData.apiId);
              } else if (defaultData.isLeaf && this.isAutoMode) {
                this.$nextTick(() => {
                  this.id = defaultData.apiId;
                });
              }
            }
            if (treeData.length == 0) {
              this.apiDetail = {};
            }
          });

          // 第一次加载
          if (!isGetMore) {
            resolve(treeData);
          } else {
            treeData.forEach((item) => {
              if (node && node.parent) {
                this.$refs.tree.append(item, node.parent.data);
              }
            });
          }
        })
        .catch((err) => {
          this.apiDetail = {};
          this.$message.error(err.msg);
        });
    },
    
// 点击加载更多
    getNodeMore(node) {
      this.$refs.more.style.display = 'none';
      const urlNode = node.parent.data.urlNode;
      const urlPath = node.parent.data.urlPath;
      this.pageMap.set(
        `${urlNode}_${urlPath}`,
        this.pageMap.has(`${urlNode}_${urlPath}`)
          ? this.pageMap.get(`${urlNode}_${urlPath}`) + 1
          : this.pageCount
      );
      this.getTree(
        {
          urlNode: urlNode,
          urlPath: urlPath,
          page: this.pageMap.get(`${urlNode}_${urlPath}`)
        },
        this.resolve,
        node,
        true
      );
    }

思路二主要代码:

文中一些未出现的方法,为实际项目中使用到的方法、参数,使用时替换为自己的即可

<div class="tree-container" v-loading="isLoading">
            <qz-pro-virtual-tree
              lazy
              highlight-current
              ref="tree"
              class="tree"
              node-key="apiId"
              icon-class="expand-icon"
              :indent="0"
              :props="props"
              :load="loadNode"
              :default-expanded-keys="defaultExpandedKeys"
              @node-click="handleNodeClick"
            >
              <div class="row-context" slot-scope="{ node, data }">
                <qz-icon
                  class="icon-style icon-folder"
                  v-if="!node.isLeaf"
                ></qz-icon>
                <span
                  class="method-style"
                  v-if="data.urlCount === 0"
                  :class="{
                    red: data.methods && data.methods[0] == 'POST',
                    green: data.methods && data.methods[0] == 'GET'
                  }"
                >
                  【{{ data.methods && data.methods[0] }}】
                </span>
                <el-tooltip
                  class="item"
                  placement="top"
                  v-if="data.urlCount === 0"
                >
                  <div slot="content">
                    <qz-copy btn-class="copy-btn">{{ data.url }}</qz-copy>
                  </div>
                  <span class="url">{{ data.label }}</span>
                </el-tooltip>
                <span
                  v-else
                  :class="data.label == '加载更多' ? 'get-more' : ''"
                >
                  {{ data.label }}
                  <i
                    v-if="data.label == '加载更多'"
                    class="el-icon-d-arrow-left"
                    style="transform: rotate(-90deg)"
                  ></i>
                </span>
                <!-- 去拆分区合并按钮 -->
                <div class="api-choice" v-if="data.urlCount === 0">
                  <span class="api-choice--right">
                    <el-tooltip
                      class="item"
                      effect="dark"
                      content="去拆分"
                      placement="top"
                    >
                      <qz-icon
                        class="icon-split color-main"
                        @click.native="goSplit(data.url)"
                      ></qz-icon>
                    </el-tooltip>
                    <el-tooltip
                      class="item"
                      effect="dark"
                      content="去合并"
                      placement="top"
                    >
                      <qz-icon
                        @click.native="goMerge(data.url)"
                        class="icon-join color-main"
                      ></qz-icon>
                    </el-tooltip>
                  </span>
                </div>
              </div>
            </qz-pro-virtual-tree>
          </div>
// 点击加载更多
    getNodeMore(node) {
      let nodePage = this.handleNodePage('get', node);
      let hasNodePage = this.handleNodePage('has', node);
      this.handleNodePage(
        'set',
        node,
        hasNodePage ? nodePage + 1 : this.pageCount
      );
      this.getTree(
        {
          urlNode: node.parent.data.urlNode,
          urlPath: node.parent.data.urlPath,
          page: this.handleNodePage('get', node),
          urlPathList: node.data.urlPathList
        },
        this.resolve,
        node,
        true
      );
    },
    // 树中添加加载更多节点
    addGetMoreNode(node, treeData) {
      if (!this.handleNodePage('has', node)) {
        this.handleNodePage('set', node, 1);
      }
      treeData.push({
        apiId: `${node.data.urlPath}_loadMore_${this.handleNodePage(
          'get',
          node
        )}`,
        id: `${node.data.urlPath}_loadMore_${this.handleNodePage('get', node)}`,
        isLeaf: true,
        urlNode: 2,
        urlCount: '',
        urlPath: node.data.urlPath,
        urlPathList: node.data.urlPathList,
        isGetMore: true
      });
    },
    // 获取树形数据
    async getTree(params, resolve, node, isGetMore = false) {
      if (node.level === 0) {
        this.isLoading = true;
      }
      Object.assign(this.condition, params);
      let postParams = JSON.parse(JSON.stringify(this.condition));
      postParams = this.handleTreeParams(postParams);
      this.searchParams = postParams;
      await getAppStructureTreeData(postParams)
        .then((res) => {
          let activeIndex = undefined;
          let treeData = res.data || [];
          if (treeData.length == 20) {
            this.addGetMoreNode(node, treeData);
          }
          treeData.forEach((item, index) => {
            item['_locateIndex'] = index;
            item.isLeaf = item.urlCount == 0;
            item.url = formatUri(item.apiUri);
            item.apiId = item.apiId || item.urlPath;
            if (item.isLeaf) {
              item.label = item.isGetMore ? '加载更多' : item.urlPath;
            } else {
              item.label = `${item.urlPath}${item.urlCount})`;
            }
            if (treeData.length > 0) {
              activeIndex =
                this.siblingType == 'prev'
                  ? treeData.length - 1
                  : activeIndex || 0;
              let defaultData = treeData[activeIndex];
              if (!defaultData.isLeaf && this.isAutoMode) {
                this.defaultExpandedKeys.push(defaultData.apiId);
              } else if (defaultData.isLeaf && this.isAutoMode) {
                this.$nextTick(() => {
                  this.id = defaultData.apiId;
                });
              }
            }
            if (treeData.length == 0) {
              this.apiDetail = {};
            }
          });
          if (!isGetMore) {
            resolve(treeData);
          } else {
            treeData.forEach((item) => {
              if (node && node.parent) {
                this.$refs.tree.append(item, node.parent.data);
              }
            });
            node && this.$refs.tree.remove(node.data);
          }
        })
        .catch((err) => {
          this.apiDetail = {};
          this.$message.error(err.msg);
        });
      this.isLoading = false;
      this.searchDisabled = false;
    },
    // 加载节点
    loadNode(node, resolve) {
      getAppUriData(this.$route.query.appUri).then((res) => {
        // 渲染第一级
        if (node.level === 0) {
          this.getTree(
            {
              urlNode: 0,
              urlPath: res.data.app.host,
              parentPath: node.parent?.data?.urlPath || '',
              page: 1,
              urlPathList: []
            },
            resolve,
            node
          );
          this.node = node;
          this.resolve = resolve;
        }
        if (node.isLeaf) {
          return resolve([]);
        }
        // 处理其余节点
        if (node.level > 0) {
          this.getTree(
            {
              urlNode: node.data.urlNode,
              urlPath: node.data.urlPath,
              parentPath: node.parent?.data?.urlPath || '',
              page: 1,
              urlPathList: node.data.urlPathList
            },
            resolve,
            node
          );
        }
      });
    },
    // 点击节点
    handleNodeClick(data, Node) {
      this.isAutoMode = false;
      if (Node.isLeaf && !data.isGetMore) {
        this.id = data.apiId;
      }
      if (data.isGetMore) {
        this.getNodeMore(Node);
      }
    },
// 操作页数
    handleNodePage(type, node, val = '') {
      const urlNode = node.parent.data.urlNode;
      const urlPath = node.parent.data.urlPath;
      if (type === 'get') {
        return this.pageMap.get(`${urlNode}_${urlPath}`);
      } else if (type === 'set') {
        return this.pageMap.set(`${urlNode}_${urlPath}`, val);
      } else {
        return this.pageMap.has(`${urlNode}_${urlPath}`);
      }
    },