Open lostvita opened 5 years ago
书承上文,在前文【Vue进阶】青铜选手,如何自研一套UI库中介绍了Vue组件库的开发细节,举例实现了button、table等组件的开发。在Ange这个UI库中,我实现了一个内容高可定制的表格组件:可固定表头和表列,内容则自行定义。
首先要承认,这个table组件实现的功能很简单:
表格组件是UI库里面最为复杂的组件之一,项目中使用表格的场景特别多,我们很难覆盖所有人的需求,比较常见的就有:
从作用对象来看,这些需求又可归为影响布局(Eg: 固定表头表列)和影响数据(Eg: 勾选数据)两个大类。在*Ange UI的table组件中,仅仅实现了影响布局这个类下面的部分功能,该组件不操作数据,甚至具体到使用tr、td标签(以及td里面如何包裹数据)展示数据也是由使用者自己定义的。狠狠点击这里在线查看示例,或者查看代码:
<ag-table offsetTop="57.5"> <tr slot="thead"> <!-- 定义表头列 --> <th v-if="isExpand">姓名</th> <th v-for="(each, index) in singleTableHead" :key="index">{{ each }}</th> </tr> <tr v-for="(each, index) in singleTableBody" slot='tbody' :key="`tbody-${index}`"> <!-- 渲染表体内容 --> <td v-if="isExpand">{{ each.name }}</td> <td>{{ each.verdict }}</td> <td>{{ each.song }}</td> </tr> </ag-table>
通过插槽slot指定thead或是tbody。简单就意味着精细和可拓展性强,同时带来的问题就是用户的使用成本高了(比如实现数据选择功能,当然ag-table在不操作源数据的原则下也能拓展出这个功能)。
ag-table
从浏览器中审查Element table组件的渲染效果看,Element实现固定表头表列的方式是:将固定的部分(如表头)和不固定的部分(如表体)拆分放在不同区域(不同的div下),设置表体所在区域可滚动即可,然后再通过一定的手段(如阴槽、表格数据备份)去同步不同区域之间的布局。
在一篇饿了么专题的文章中,详细阐述了固定表头表列的实现。下面简单总结并整理其中存在的问题。
从浏览器中审查table组件的渲染效果看: 表头和表体分别放在了两个不同的div区域:el-table__header-wrapper & el-table__body-wrapper,如此表体内容超出容器高度时,会出现滚动条,只在自己区域内滚动,达到了表头固定的效果。这样的实现导致了两个问题:
el-table__header-wrapper
el-table__body-wrapper
针对上面的问题,element也做了处理,引用饿了么文中一张图片: 在表头部分增加一个Gutter元素,虚拟成滚动条去占据一定宽度(图片右上角粉色的竖条),宽度一致的处理则是要求用户使用的时候个传入每个列的宽度。
这种实现方式有什么缺点呢?
实现固定表列相对比较复杂,为实现这个功能,element可谓是付出了“巨大的成本”。在这个左右列固定的渲染效果中: 渲染出了3份表格:el-table__header-wrapper & el-table__body-wrapper 是表体区域,el-table__fixed 是左固定列区域、el-table__fixed-right 是右固定列区域),每一份表格又有2个table,一共是6个table;通过设置左右区域绝对定位和宽度实现固定的效果。
el-table__fixed
el-table__fixed-right
这样实现会有什么问题呢?
基于此,Ange UI的table实现考虑用另外一种方式去实现,达到了最低的DOM成本。
在介绍固定表头表列实现方法之前,先科普下getBoundingClientRect这个API。
getBoundingClientRect()方法返回元素的大小及其相对视口的位置,它的返回值是一个DOMRect对象。DOMRect对象包含了一组用于描述边框的只读属性:left、right、top、bottom,单位为像素。除了width和height外的属性都是相对于视口的左上角而言的。
如下图:
在一个table中分别用thead和tbody展示表头表体,如下代码:
<template> <div class="ange-table"> <table ref="middle-table"> <thead class="thead-middle" :style="theadStyle"> <slot name="thead" /> </thead> <tbody> <slot name="tbody" /> </tbody> </table> </div> </template>
监听页面滚动事件,计算table的位移,使用translate3d反向设置thead的y轴位移值,达到固定表头的效果。如下图: 滚动页面滚动条,table由top1(正值)位置移动到top2(负值)位置,那么,要使thead在触碰到页面顶端时(即top=0),继续移动,thead就要设置成 translate3d(0px, -top2, 0px)。这样,thead就一直处在页面顶端位置了。 在某些场景下,thead达到Header的位置时就应该被fixed了,那们我们可以设置一个offsetTop参数,用户自定义偏移值,thead在top=0 - offsetTop时被fixed。看关键实现代码:
translate3d
translate3d(0px, -top2, 0px)
offsetTop
top=0 - offsetTop
export default { data () { return: { fixed: { // fixed状态 top: false }, clientRect: { // 位移值 top: 0 } } }, computed: { theadStyle () { const { top } = this.clientRect return { transform: `translate3d(0px, ${this.fixed.top ? -top : 0}px, 1px)` } } }, watch: { 'clientRect.top': function (val) { // 判断到DOMRect的top值小于0时,开始fixed this.fixed.top = val < 0 } }, mounted () { // 监听页面滚动事件,获取table对象的DOMRect属性 window.addEventListener('scroll', this.scrollHandle, { capture: false, passive: true }) }, methods: { scrollHandle () { const $table = this.$refs.table if(!$table) return const { top } = $table.getBoundingClientRect() this.clientRect.top = Math.floor(top - parseInt(this.offsetTop, 10)) } } }
结合 @前言 部分ag-table的使用示例,在<ag-tbale>中传入一个offsetTop参数,即可实现thead在指定位置的fixed。另由于thead和tbody在同一个table中,不需要维护每一列的宽度,它可以根据内容自适应。查看demo。
<ag-tbale>
固定列的实现需要三个表格(分别固定左列和右列),如下代码:
<template> <div class="ange-table"> <!-- left table --> <table v-if="hasLeftTable" ref="leftTable" :style="leftStyle"> <thead class="thead-left" :style="theadStyle"> <slot name="leftThead" /> </thead> <tbody> <slot name="leftBody" /> </tbody> </table> <!-- middle table --> <table ref="table" class="table-middle"> <thead class="thead-middle" :style="theadStyle"> <slot name="thead" /> </thead> <tbody> <slot name="tbody" /> </tbody> </table> <!-- right table --> <table v-if="hasRightTable" ref="rightTable" :style="rightStyle"> <thead class="thead-right" :style="theadStyle"> <slot name="rightThead" /> </thead> <tbody> <slot name="rightBody" /> </tbody> </table> </div> </template>
table横向滚动时,计算容器的横向滚动距离scrollLeft,使用translate3d反向设置左table的x轴位移值,固定左列;对于右table,先要将其初始位置设置在容器的最右端,横向滚动时再结合scrollLeft设置x轴的位移值。如下图: 初始化时,rightTable的横向位移值:$rightTable.right - $container.right,leftTable就是0;发生横向滚动时,leftTable的横向位移值:scrollLeft,rightTable的位移值:初始位移 - scrollLeft。看关键实现代码:
scrollLeft
$rightTable.right - $container.right
初始位移 - scrollLeft
export default { computed: { leftStyle () { // 左侧表格位移 const { left } = this.clientRect return { transform: `translate3d(${this.fixed.left ? left : 0}px, 0px, 1px)` } }, rightStyle () { // 右侧表格位移 const { right } = this.clientRect return { transform: `translate3d(${-right}px, 0px, 1px)` } } }, watch: { 'clientRect.left': function (val) { // 横向滚动距离为正,开始设置fixed this.fixed.left = val > 0 } }, mounted () { // 存在由表格时设置其初始位移 if(this.hasRightTable) { const container = this.$refs.container.getBoundingClientRect() const rightTable = this.$refs.rightTable.getBoundingClientRect() this.clientRect.right = Math.floor(rightTable.right - container.right) // 记录右表格初始位移值 this.initRight = this.clientRect.right } // 监听表格容器的滚动事件 this.$refs.container.addEventListener('scroll', this.scrollXHandle, { capture: false, passive: true }) // ... }, methods: { scrollXHandle () { // ... this.clientRect.left = Math.floor(this.$refs.container.scrollLeft) const right = Math.floor(this.initRight - this.$refs.container.scrollLeft) this.clientRect.right = right } } }
按照这个思路实现左右列固定,效果如下(在线查看):
最后一步,因为这个表格是由三份table组成,因此当鼠标hover在其中一个table行上时,需要在剩余两个table的对应行中同步hover效果。看关键代码的实现:
export default { mounted () { if(this.hasLeftTable || this.hasRightTable) { // 定义鼠标hover事件 this.$el.addEventListener('mouseover', this.mouseOver, false) this.$el.addEventListener('mouseout', this.mouseLeave, false) } }, methods: { mouseOver (e) { this.hoverClass(e, 'add') }, mouseLeave(e) { this.hoverClass(e, 'remove') }, hoverClass(e, type) { const tr = e.target.closest('tr') if(!tr) { return } const idx = tr.rowIndex // 当前hover行的编号 const trs = querySelectorAll(`tbody tr:nth-child(${idx})`, this.$el) if(trs.length === 0) { return } // 对所有tbody下同一编号的tr添加hover类 trs.forEach(each => { each.classList[type]('hover') }) } } }
通过translate3d设置左右列的位移实现固定列的效果,避免了:
table组件一直是开发复杂度较高的组件,既要考虑性能,又要考虑尽可能地对开发者使用友好。在此抛砖引玉,提供另一种开发思路,只为给有计划开发table组件的你提供一点帮助。
当然你有其他的想法欢迎评论一起交流~
The end.
写得很好,即使现在看,也是很详尽的blog
前言
书承上文,在前文【Vue进阶】青铜选手,如何自研一套UI库中介绍了Vue组件库的开发细节,举例实现了button、table等组件的开发。在Ange这个UI库中,我实现了一个内容高可定制的表格组件:可固定表头和表列,内容则自行定义。
首先要承认,这个table组件实现的功能很简单:
表格组件是UI库里面最为复杂的组件之一,项目中使用表格的场景特别多,我们很难覆盖所有人的需求,比较常见的就有:
从作用对象来看,这些需求又可归为影响布局(Eg: 固定表头表列)和影响数据(Eg: 勾选数据)两个大类。在*Ange UI的table组件中,仅仅实现了影响布局这个类下面的部分功能,该组件不操作数据,甚至具体到使用tr、td标签(以及td里面如何包裹数据)展示数据也是由使用者自己定义的。狠狠点击这里在线查看示例,或者查看代码:
通过插槽slot指定thead或是tbody。简单就意味着精细和可拓展性强,同时带来的问题就是用户的使用成本高了(比如实现数据选择功能,当然
ag-table
在不操作源数据的原则下也能拓展出这个功能)。谈谈element的固定表头表列
从浏览器中审查Element table组件的渲染效果看,Element实现固定表头表列的方式是:将固定的部分(如表头)和不固定的部分(如表体)拆分放在不同区域(不同的div下),设置表体所在区域可滚动即可,然后再通过一定的手段(如阴槽、表格数据备份)去同步不同区域之间的布局。
在一篇饿了么专题的文章中,详细阐述了固定表头表列的实现。下面简单总结并整理其中存在的问题。
1.1 固定表头的思路
从浏览器中审查table组件的渲染效果看:
表头和表体分别放在了两个不同的div区域:
el-table__header-wrapper
&el-table__body-wrapper
,如此表体内容超出容器高度时,会出现滚动条,只在自己区域内滚动,达到了表头固定的效果。这样的实现导致了两个问题:针对上面的问题,element也做了处理,引用饿了么文中一张图片:
在表头部分增加一个Gutter元素,虚拟成滚动条去占据一定宽度(图片右上角粉色的竖条),宽度一致的处理则是要求用户使用的时候个传入每个列的宽度。
这种实现方式有什么缺点呢?
1.2 固定表列的思路
实现固定表列相对比较复杂,为实现这个功能,element可谓是付出了“巨大的成本”。在这个左右列固定的渲染效果中:
渲染出了3份表格:
el-table__header-wrapper
&el-table__body-wrapper
是表体区域,el-table__fixed
是左固定列区域、el-table__fixed-right
是右固定列区域),每一份表格又有2个table,一共是6个table;通过设置左右区域绝对定位和宽度实现固定的效果。这样实现会有什么问题呢?
基于此,Ange UI的table实现考虑用另外一种方式去实现,达到了最低的DOM成本。
getBoundingClientRect
在介绍固定表头表列实现方法之前,先科普下getBoundingClientRect这个API。
如下图:![image](https://user-images.githubusercontent.com/26505680/59407380-99446600-8de3-11e9-8db7-75e021ff16c4.png)
实现固定表头
在一个table中分别用thead和tbody展示表头表体,如下代码:
监听页面滚动事件,计算table的位移,使用
滚动页面滚动条,table由top1(正值)位置移动到top2(负值)位置,那么,要使thead在触碰到页面顶端时(即top=0),继续移动,thead就要设置成
translate3d
反向设置thead的y轴位移值,达到固定表头的效果。如下图:translate3d(0px, -top2, 0px)
。这样,thead就一直处在页面顶端位置了。 在某些场景下,thead达到Header的位置时就应该被fixed了,那们我们可以设置一个offsetTop
参数,用户自定义偏移值,thead在top=0 - offsetTop
时被fixed。看关键实现代码:结合 @前言 部分ag-table的使用示例,在
<ag-tbale>
中传入一个offsetTop
参数,即可实现thead在指定位置的fixed。另由于thead和tbody在同一个table中,不需要维护每一列的宽度,它可以根据内容自适应。查看demo。实现固定表列
固定列的实现需要三个表格(分别固定左列和右列),如下代码:
table横向滚动时,计算容器的横向滚动距离
初始化时,rightTable的横向位移值:
scrollLeft
,使用translate3d
反向设置左table的x轴位移值,固定左列;对于右table,先要将其初始位置设置在容器的最右端,横向滚动时再结合scrollLeft设置x轴的位移值。如下图:$rightTable.right - $container.right
,leftTable就是0;发生横向滚动时,leftTable的横向位移值:scrollLeft
,rightTable的位移值:初始位移 - scrollLeft
。看关键实现代码:按照这个思路实现左右列固定,效果如下(在线查看):![image](https://user-images.githubusercontent.com/26505680/59407614-5e8efd80-8de4-11e9-9f3a-aae9e155064f.png)
同步Hover效果
最后一步,因为这个表格是由三份table组成,因此当鼠标hover在其中一个table行上时,需要在剩余两个table的对应行中同步hover效果。看关键代码的实现:
通过
translate3d
设置左右列的位移实现固定列的效果,避免了:结语
table组件一直是开发复杂度较高的组件,既要考虑性能,又要考虑尽可能地对开发者使用友好。在此抛砖引玉,提供另一种开发思路,只为给有计划开发table组件的你提供一点帮助。
当然你有其他的想法欢迎评论一起交流~
The end.