目录
- BUG描述
- el-table 树表格load源码
- 目前的解决方法
- 源码bug修复
- element-plus 源码调试
- 如何提交PR
- 总结
BUG描述
最近公司新开了一个项目,需要用到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 的源码。
el-table 树表格load源码
首先,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中存的数据即可。
源码bug修复
既然找到问题所在,不如动动手解决一下,万一能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 源码调试
接下来就是调试,看看有没有效果。
- pnpm i
- npm run dev
调试的文件在:
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,要不是新坑,我也不会想到要提,也算是攒了个经验。
介绍下提交PR的步骤:
- fork源码仓库
- clone 自己fork后的仓库
- 在本地打开git 与fork仓库建立连接
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
- 提交PR
等待review即可。
总结
以上就是我最近遇到问题以及解决问题的全过程,记录下。
解决的方法不一定好,但是,目前暂时想不到其他好的方式,对于element-plus的源码研究也就是个皮毛的程度,看能不能merge吧。