Vue3 组件二次封装 Element Plus Table
公司里后台系统用的 Element UI
,有百来个表格(el-table),历史遗留原因都是直接使用 el-table 的。突然有一天,产品说表格要可以自定义列,让用户控制列的显隐、固定和排序,最好还能持久储存。使得我不得不进行二次封装来解决,那就顺便再轻微增强一下。
Demo:element-plus-table-proxy-demo
源码:aweikalee/element-plus-table-proxy-demo
Vue 2
+Element UI
请前往 《Vue2 组件二次封装 Element UI El-Table》
主要思路
对于 el-table 的二次封装,我希望是:
- 不对原有的表产生影响(过度阶段 不可能一次性改完所有表)。
- 尽可能保留 el-table 本身的灵活性。
- 增强表格功能的同时,尽可能少地动原先的代码。
对于第1点,则是保留 el-table 组件,创建新组件 MyTable,所有改动在这个新组件内部完成。
对于第2点,就是 MyTable 接受的 props(attrs)
和 slot
应与 el-table 保持一致,且应悉数传递给 el-table。
于是设计的调整方案如下:
1 | <!-- 调整前 --> |
新封装的组件 MyTable 所做的事很简单,就是对 slot
重新排序、筛选、修改属性之后,生成一个新的 slot
再交给 el-table 处理。
MyTable 会给 MyToolbar 暴露列的数据与修改列数据的接口。(当然你也可以将 MyToolbar 封装 MyTable 内)
MyTable 组件的实现
基本结构
首先是 template
部分(当然你可以用 render/JSX
代替),Vue
中默认会传递所有未识别的属性给最外层的标签,所以我们只需要传一个新的 slot
就可以了。
1 | <el-table> |
children 就是我们实现的新的 slot
,他是 MyTable 内部创建的子组件。他和 slots.default
一样是一个函数,里面返回了 VNode
。
1 | const slotsOrigin = useSlots() |
注:用了 setup 语法
至此,一个保留了 el-table 所有功能的二次封装,就完成了。接下来只需要再加亿点点细节完善一下。
对 VNode 分类
从 slot
中获取到的 VNode
除了我们要的内容外,还会有些其他东西,所以我们需要进行分类。
对于 el-table-column 的 VNode
的处理,将会以 prop
属性作为标识。没有 prop
属性的则不会作为自定义列做处理。
VNode
将会被分成3类:
- el-table-column 且有
prop
属性的 - el-table-column 但没有
prop
属性,但fixed="left"
的 - 其他的 el-table-column 或不认识的
VNode
第2类,也可以并到第3类中,但我认为分成3类更符合实际需求。
1 | const slotsOrigin = useSlots() |
收集列数据
列数据的一手来源,就是 slots.main
。因此需要从 VNode
中提取出我们需要的属性和排列顺序。
1 | const columns = reactive({ |
除了 visiable
外都是 el-table-column 原有的属性。columns.slot
只保存最原始的列数据,我们对于列的修改,需要保存在另外的地方,后续还要做持久化储存,所以就存在了 columns.storage
中。
对外提供一个修改 columns.storage
的方法。
1 | function updateColumns(value) { |
合并列数据
现在我们有两个数据 columns.slot
与 columns.storage
,考虑到持久化储存,储存的列的信息可能不准确(如后期新增/删除了列),取长补短,获得一个渲染时用的完整的列数据。
1 | const columns = reactive({ |
生成新的 VNode
前期准备都做好了,现在需要创建传给 el-table 的 slot
了。
我们需要以 columns.render
的数据创建 refactorSlot
代替 slots.main
。
1 | const refactorSlot = computed(() => { |
最后合并所有 slot
,就完成了 children
的创建
1 | const children = () => [slots.value.left, refactorSlot.value, slots.value.other] |
更新列数据
el-table-column 是通过 onMounted
与 onUnmounted
两个生命周期将列数据同步给 el-table 的。但 Vue
会尽可能利用旧的实例,只会更新实例上的数据,而不是销毁重新创建。这就导致 onMounted
与 onUmmounted
无法运行,从而会产生 el-table 中的列数据与 el-table-column 不一致。
故此处通过更新 key
来强制重新创建 el-table-column。
1 | <el-table> |
1 | const key = ref(0) |
暴露接口
1 | <el-table ref="table"> |
1 | const table = ref() |
至此,我们主体结构就搭完了,完整代码可以到 aweikalee/element-plus-table-proxy-demo 查看。
追加功能
接下来就是追加各种功能。
MyToolbar 组件的实现
MyTable 对外提供了 columns
与 updateColumns
,通过它们我们可以根据需求实现一个自定义列的显示、固定和排序。由于这边怎么实现都行,就不细说了。aweikalee/element-plus-table-proxy-demo 中有简单的实现可以参考。
列数据持久化储存
只要让 columns.storage
初始化时从 localStorage
中获取,修改时写入 localStorage
即可。
1 | // 实现一个简易版本,意思一下。 |
1 | const columnsFormStorage = ref( |
这边 stroage.get('columns')
并没有对表格进行区分储存。可以为 MyTable 增加一个属性 name
,储存与读取时以 name
做为标识以区分。
当然列的设置是可以存服务器,意味着储存都是异步的,读取时请求返回之前,会进行一次渲染,请求返回后会再次渲染,这是需要特别注意的。我选择了请求完成前不渲染
children
,而是使用加载的状态代替。上传则采用了防抖的方式减少与服务器交互。
KeepAlive 保留滚动条位置
尽管 KeepAlive 会缓存 DOM,但 DOM 会从文档上移除。而离开文档的 DOM 是没有 offsetTop
, offsetLeft
, offsetWidth
, offsetHeight
, scrollTop
, scrollWidth
, scrollHeight
, clientWidth
, clientHeight
的,此时访问到的也都是 0
。
在 KeepAlive 中最受影响的就是 scrollTop
和 scrollLeft
,即使重新添加到文档中也无法恢复。所以我们需要在离开文档前保存它们,重新添加到文档后将保存的值再赋值到 DOM 上。
下面介绍两种方法。
方法一
监听 DOM 的 scroll
事件,scroll
事件中记录当前的滚动位置。然后在 onActivated
时重新给 DOM 赋值。
1 | <el-table ref="table"></el-table> |
1 | const table = ref() |
1 | function useKeepScroll(el) { // 这是一个 ref 对象 |
方法二
KeepAlive 为我们提供了 onDeactivated
,但它定义就是 DOM 停用后的生命周期,所以 onDeactivated
运行的时候 DOM 已经从文档中移除了。
我们可能更需要 onBeforeDeactivate
,但是很可惜,该 RFC 还没有实装。
当前的代替方案,有那么点取巧。
1 | function useKeepScroll(el) { |
接下来是关键了!
1 | <Transition> |
找到使用 KeepAlive 的地方,在外面套一层 Transition 组件,此时 onDeactivated
就等同于 onBeforeDeactivate
了。
若你的项目只存在一个 KeepAlive,就非常适合用这种解决方法。
简单解释一下原理:
KeepAlive 组件的 deactivate
方法中,会先将 DOM 从文档中移除,再创建微任务调用组件的 onDeactivated
。若 VNode
上存在 transition
,移除将会是变为宏任务,那么就会变成先执行微任务中的 onDeactivated
再从文档中移除了。