赞
踩
最近公司新开了一个项目,需要用到Vue3+ts,UI框架使用element-plus。
说实话,vue3出来这么久,2023年我才用上实际项目。
项目上有这样一个场景,需要用到树表格且懒加载,还要支持子节点的增、删、改。
el-table树表格懒加载目前的实现方法是通过调用load,节点修改时,再次手动调用。
这是官网的例子:
<template> <div> <el-table :data="tableData1" style="width: 100%" row-key="id" border lazy :load="load" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" > <el-table-column prop="date" label="Date" /> <el-table-column prop="name" label="Name" /> <el-table-column prop="address" label="Address" /> </el-table> </div> </template> <script lang="ts" setup> interface User { id: number date: string name: string address: string hasChildren?: boolean children?: User[] } const load = ( row: User, treeNode: unknown, resolve: (date: User[]) => void ) => { setTimeout(() => { resolve([ { id: 31, date: '2016-05-01', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, { id: 32, date: '2016-05-01', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, ]) }, 1000) } const tableData1: User[] = [ { id: 1, date: '2016-05-02', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, { id: 2, date: '2016-05-04', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, { id: 3, date: '2016-05-01', name: 'wangxiaohu', hasChildren: true, address: 'No. 189, Grove St, Los Angeles', }, { id: 4, date: '2016-05-03', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, ] </script>
里面并没有涉及这种场景,因此,在下面向百度编程得知,手动调用load的方式为:
// 先new一个map用于数据保存 const rowMaps = reactive(new Map()) // 加载子级时保存数据 const load = async (row: any, treeNode: unknown, resolve: (data: any[]) => void) => { let re = await getTreeChild({ parentId: row.id }) resolve(re.data) rowMaps.set(row.id, { row, treeNode, resolve, children: re.data }) } // 刷新时 const refresh = (parentId: any) => { if (rowMaps.get(parentId)) { parentId = parentId ? parseInt(parentId) : 0 // 获取相关数据并load const { row, treeNode, resolve } = rowMaps.get(parentId) load(row, treeNode, resolve) } else { getList() } }
如此以来,增、删、改后动态更新子节点的问题就解决了,就当我愉快地准备进行下一项工作时,发现了一个不得了的bug。
而我面向百度编程了好久,都没有得到解决。
这个新坑怕是被我踩到了。
于是去扒起了element-plus 的源码。
首先,load可以执行,但是只剩一个子节点就有问题,那么就直接可以定位bug在load方法里:
文件路径:element-plus\packages\components\table\src\store\tree.ts
const loadData = (row: T, key: string, treeNode) => { const { load } = instance.props as unknown as TableProps<T> if (load && !treeData.value[key].loaded) { treeData.value[key].loading = true load(row, treeNode, (data) => { if (!Array.isArray(data)) { throw new TypeError('[ElTable] data must be an array') } treeData.value[key].loading = false treeData.value[key].loaded = true treeData.value[key].expanded = true // 就是这里,我们的子节点删完了data.length == 0,无法赋值 if (data.length) { lazyTreeNodeMap.value[key] = data } instance.emit('expand-change', row, true) }) } }
PR已经提了,还在review,目前碰到的话用这种方式:
// 刷新时
const refresh = (parentId: any) => {
if (rowMaps.get(parentId)) {
parentId = parentId ? parseInt(parentId) : 0
// 获取相关数据并load
const { row, treeNode, resolve } = rowMaps.get(parentId)
// 源码bug,需要先置空
tableRef.value.store.states.lazyTreeNodeMap.value[parentId] = []
load(row, treeNode, resolve)
} else {
getList()
}
}
置空lazyTreeNodeMap.value中存的数据即可。
既然找到问题所在,不如动动手解决一下,万一能merge,我也算是开源项目的贡献者之一了。这个成就,还是蛮不错的。哈哈。
我的想法是:
// 源码bug,需要先置空
tableRef.value.store.states.lazyTreeNodeMap.value[parentId] = []
这段语句实在是太难看了,这么长,最好是在源码里解决掉,外部通过一个方法调用一下,类似clear。
那么首先需要分析一下源码懒加载的实现方式。
其中tree.ts就是树表格的状态管理文件。
该文件中用updateTreeData方法实现数据更新。
const updateTreeData = ( ifChangeExpandRowKeys = false, ifExpandAll = instance.store?.states.defaultExpandAll.value ) => { const nested = normalizedData.value const normalizedLazyNode_ = normalizedLazyNode.value const keys = Object.keys(nested) const newTreeData = {} if (keys.length) { const oldTreeData = unref(treeData) const rootLazyRowKeys = [] const getExpanded = (oldValue, key) => { if (ifChangeExpandRowKeys) { if (expandRowKeys.value) { return ifExpandAll || expandRowKeys.value.includes(key) } else { return !!(ifExpandAll || oldValue?.expanded) } } else { const included = ifExpandAll || (expandRowKeys.value && expandRowKeys.value.includes(key)) return !!(oldValue?.expanded || included) } } // 合并 expanded 与 display,确保数据刷新后,状态不变 keys.forEach((key) => { const oldValue = oldTreeData[key] const newValue = { ...nested[key] } newValue.expanded = getExpanded(oldValue, key) if (newValue.lazy) { const { loaded = false, loading = false } = oldValue || {} newValue.loaded = !!loaded newValue.loading = !!loading rootLazyRowKeys.push(key) } newTreeData[key] = newValue }) // 根据懒加载数据更新 treeData const lazyKeys = Object.keys(normalizedLazyNode_) if (lazy.value && lazyKeys.length && rootLazyRowKeys.length) { lazyKeys.forEach((key) => { const oldValue = oldTreeData[key] const lazyNodeChildren = normalizedLazyNode_[key].children if (rootLazyRowKeys.includes(key)) { // 懒加载的 root 节点,更新一下原有的数据,原来的 children 一定是空数组 if (newTreeData[key].children.length !== 0) { throw new Error('[ElTable]children must be an empty array.') } newTreeData[key].children = lazyNodeChildren } else { const { loaded = false, loading = false } = oldValue || {} newTreeData[key] = { lazy: true, loaded: !!loaded, loading: !!loading, expanded: getExpanded(oldValue, key), children: lazyNodeChildren, level: '', } } }) } } treeData.value = newTreeData instance.store?.updateTableScrollY() }
可以看到数据以键值对的方式存储在变量newTreeData,通过newTreeData赋值treeData实现数据更新。
而该方法(updateTreeData)又监听了normalizedLazyNode
watch(
() => normalizedData.value,
() => {
updateTreeData()
}
)
watch(
() => normalizedLazyNode.value,
() => {
updateTreeData()
}
)
而normalizedLazyNode就是lazyTreeNodeMap.value的计算属性
const normalizedLazyNode = computed(() => { const rowKey = watcherData.rowKey.value const keys = Object.keys(lazyTreeNodeMap.value) const res = {} if (!keys.length) return res keys.forEach((key) => { if (lazyTreeNodeMap.value[key].length) { const item = { children: [] } lazyTreeNodeMap.value[key].forEach((row) => { const currentRowKey = getRowIdentity(row, rowKey) item.children.push(currentRowKey) if (row[lazyColumnIdentifier.value] && !res[currentRowKey]) { res[currentRowKey] = { children: [] } } }) res[key] = item } }) return res })
因此,我们只需置空lazyTreeNodeMap.value的相关数据,即可触发更新。
于是,新增方法clearTreeNode
// 子节点删除后手动调用更新
const clearTreeNode = (key: string) => {
if (treeData.value[key].loaded) {
lazyTreeNodeMap.value[key] = []
}
}
现在方法写好了,我想通过
tableRef.value.clearTreeNode(parentId)
//(tableRef.value as any).clearTreeNode('3')
这种形式调用,看下如何实现。
el-table 文件:element-plus\packages\components\table\src\table.vue
再顺着这个思路往源头找,
element-plus\packages\components\table\src\table\utils-helper.ts ==>
element-plus\packages\components\table\src\store\index.ts ==>
element-plus\packages\components\table\src\store\watcher.ts ==>
element-plus\packages\components\table\src\store\tree.ts
这里比较绕,画了个图
所以,将写好的方法抛出,按照引用链传递,即可。
接下来就是调试,看看有没有效果。
调试的文件在:
element-plus\play\src\App.vue
<template> <div class="play-container"> <el-button @click="refresh">refresh</el-button> <el-button @click="edit">edit</el-button> <el-table ref="tableRef" :data="tableData1" style="width: 100%" row-key="id" border lazy :load="load" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" > <el-table-column prop="date" label="Date" /> <el-table-column prop="name" label="Name" /> <el-table-column prop="address" label="Address" /> </el-table> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' interface User { id: number date: string name: string address: string hasChildren?: boolean children?: User[] } const tableRef = ref(null) // 保存有子节点的父级 const rowMaps = reactive(new Map()) let arr1 = [ { id: 31, date: '2016-05-01', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, { id: 32, date: '2016-05-01', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, ] const tableData1: User[] = [ { id: 1, date: '2016-05-02', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, { id: 2, date: '2016-05-04', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, { id: 3, date: '2016-05-01', name: 'wangxiaohu', hasChildren: true, address: 'No. 189, Grove St, Los Angeles', }, { id: 4, date: '2016-05-03', name: 'wangxiaohu', address: 'No. 189, Grove St, Los Angeles', }, ] // code here const load = ( row: User, treeNode: unknown, resolve: (date: User[]) => void ) => { setTimeout(() => { rowMaps.set(row.id, { row, treeNode, resolve, children: arr1 }) if (arr1.length) { resolve(arr1) } else { ;(tableRef.value as any).clearTreeNode(3) } }, 1000) } const edit = () => { let parentId = 3 arr1 = [ { id: 32, date: '2016-05-01', name: '111111', address: 'No. 189, Grove St, Los Angeles', }, ] const { row, treeNode, resolve } = rowMaps.get(parentId) load(row, treeNode, resolve) } const refresh = () => { let parentId = 3 // 模仿最后一个子节点删除 arr1 = [] const { row, treeNode, resolve } = rowMaps.get(parentId) load(row, treeNode, resolve) } </script> <style lang="scss"> html, body { width: 100vw; height: 100vh; margin: 0; #play { height: 100%; width: 100%; .play-container { height: 100%; width: 100%; display: flex; align-items: center; justify-content: center; } } } </style>
经测试,完全满足要求。
其实这也是我第一次向开源项目提交PR,要不是新坑,我也不会想到要提,也算是攒了个经验。
介绍下提交PR的步骤:
git remote add upstream https://github.com/element-plus/element-plus.git
git fetch upstream dev
pull request 规范
提交代码
git add .
git commit -m "feat: (components):[el-table] feat clearTreeNode function"
git push origin dev
以上就是我最近遇到问题以及解决问题的全过程,记录下。
解决的方法不一定好,但是,目前暂时想不到其他好的方式,对于element-plus的源码研究也就是个皮毛的程度,看能不能merge吧。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。