We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
书承上文,在前文【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.
The text was updated successfully, but these errors were encountered:
写得很好,即使现在看,也是很详尽的blog
Sorry, something went wrong.
No branches or pull requests
前言
书承上文,在前文【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。
如下图:
实现固定表头
在一个table中分别用thead和tbody展示表头表体,如下代码:
监听页面滚动事件,计算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。看关键实现代码:结合 @前言 部分ag-table的使用示例,在
<ag-tbale>
中传入一个offsetTop
参数,即可实现thead在指定位置的fixed。另由于thead和tbody在同一个table中,不需要维护每一列的宽度,它可以根据内容自适应。查看demo。实现固定表列
固定列的实现需要三个表格(分别固定左列和右列),如下代码:
table横向滚动时,计算容器的横向滚动距离
scrollLeft
,使用**translate3d
反向设置左table的x轴位移值**,固定左列;对于右table,先要将其初始位置设置在容器的最右端,横向滚动时再结合scrollLeft设置x轴的位移值。如下图:初始化时,rightTable的横向位移值:
$rightTable.right - $container.right
,leftTable就是0;发生横向滚动时,leftTable的横向位移值:scrollLeft
,rightTable的位移值:初始位移 - scrollLeft
。看关键实现代码:按照这个思路实现左右列固定,效果如下(在线查看):
同步Hover效果
最后一步,因为这个表格是由三份table组成,因此当鼠标hover在其中一个table行上时,需要在剩余两个table的对应行中同步hover效果。看关键代码的实现:
通过
translate3d
设置左右列的位移实现固定列的效果,避免了:结语
table组件一直是开发复杂度较高的组件,既要考虑性能,又要考虑尽可能地对开发者使用友好。在此抛砖引玉,提供另一种开发思路,只为给有计划开发table组件的你提供一点帮助。
当然你有其他的想法欢迎评论一起交流~
The end.
The text was updated successfully, but these errors were encountered: