前言

本文介绍内容包括:

  • Element UI 实现表头表列固定思考与总结
  • translate3d如何实现表头表列固定

书承上文,在前文【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在不操作源数据的原则下也能拓展出这个功能)。

谈谈element的固定表头表列

从浏览器中审查Element table组件的渲染效果看,Element实现固定表头表列的方式是:将固定的部分(如表头)和不固定的部分(如表体)拆分放在不同区域(不同的div下),设置表体所在区域可滚动即可,然后再通过一定的手段(如阴槽、表格数据备份)去同步不同区域之间的布局。

在一篇饿了么专题的文章中,详细阐述了固定表头表列的实现。下面简单总结并整理其中存在的问题。

1.1 固定表头的思路

从浏览器中审查table组件的渲染效果看:

表头和表体分别放在了两个不同的div区域:el-table__header-wrapper & el-table__body-wrapper,如此表体内容超出容器高度时,会出现滚动条,只在自己区域内滚动,达到了表头固定的效果。这样的实现导致了两个问题:

  • 两个表格宽度不一致:表体所在的区域多出了一条滚动条
  • 两个表格之间的列宽如何保持一致

针对上面的问题,element也做了处理,引用饿了么文中一张图片:

在表头部分增加一个Gutter元素,虚拟成滚动条去占据一定宽度(图片右上角粉色的竖条),宽度一致的处理则是要求用户使用的时候个传入每个列的宽度

这种实现方式有什么缺点呢?

  • 额外维护新增元素(Gutter);
  • 自定义每列宽度增加用户使用成本,理想情况应该能根据文本内容自适应;
  • 表体的滚动条上不去(滚不到表头的顶部),这个让我很捉急;
  • 表头仅是相对于表体的固定,能实现相对于窗口的fixed吗?

1.2 固定表列的思路

实现固定表列相对比较复杂,为实现这个功能,element可谓是付出了“巨大的成本”。在这个左右列固定的渲染效果中:

渲染出了3份表格el-table__header-wrapper & el-table__body-wrapper 是表体区域,el-table__fixed 是左固定列区域、el-table__fixed-right 是右固定列区域),每一份表格又有2个table,一共是6个table;通过设置左右区域绝对定位和宽度实现固定的效果。

这样实现会有什么问题呢?

  • 一份表格数据被渲染成三份,放大了三倍的DOM开销。(这也是element -table在数据量大或者未分页的情况下,页面卡顿,性能降低的根本原因)
  • 同步鼠标的scroll事件:在一个区域内滚动需要在其他两个区域作同步滚动
  • 额外维护固定列样式和内容(如宽度等)

基于此,Ange UI的table实现考虑用另外一种方式去实现,达到了最低的DOM成本。

getBoundingClientRect

在介绍固定表头表列实现方法之前,先科普下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。看关键实现代码:

export default {data () {return: {fixed: { // fixed状态top: false},clientRect: { // 位移值top: 0}}},computed: {theadStyle () {const { top } = this.clientRectreturn {transform: `translate3d(0px, ${this.fixed.top? -top: 0}px, 1px)`}}},watch: {'clientRect.top': function (val) {// 判断到DOMRect的top值小于0时,开始fixedthis.fixed.top = val < 0}},mounted () {// 监听页面滚动事件,获取table对象的DOMRect属性window.addEventListener('scroll', this.scrollHandle, {capture: false,passive: true})},methods: {scrollHandle () {const $table = this.$refs.tableif(!$table) returnconst { top } = $table.getBoundingClientRect()this.clientRect.top = Math.floor(top - parseInt(this.offsetTop, 10))}}
}
复制代码

结合 @前言 部分ag-table的使用示例,在<ag-tbale>中传入一个offsetTop参数,即可实现thead在指定位置的fixed。另由于thead和tbody在同一个table中,不需要维护每一列的宽度,它可以根据内容自适应。查看demo。

实现固定表列

固定列的实现需要三个表格(分别固定左列和右列),如下代码:

<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。看关键实现代码:

export default {computed: {leftStyle () { // 左侧表格位移const { left } = this.clientRectreturn {transform: `translate3d(${this.fixed.left? left: 0}px, 0px, 1px)`}},rightStyle () { // 右侧表格位移const { right } = this.clientRectreturn {transform: `translate3d(${-right}px, 0px, 1px)`}}},watch: {'clientRect.left': function (val) {// 横向滚动距离为正,开始设置fixedthis.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}}
}
复制代码

按照这个思路实现左右列固定,效果如下(在线查看):

同步Hover效果

最后一步,因为这个表格是由三份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设置左右列的位移实现固定列的效果,避免了:

  • 多余的DOM开销:不需要新增额外DOM元素(Gutter),更需要复制多份DOM数据,将DOM开销减少到最小;
  • 不需要维护不同表格之间列宽行高问题,完全自适应;
  • 不需要在多个表格之间同步scroll事件

结语

table组件一直是开发复杂度较高的组件,既要考虑性能,又要考虑尽可能地对开发者使用友好。在此抛砖引玉,提供另一种开发思路,只为给有计划开发table组件的你提供一点帮助。

当然你有其他的想法欢迎评论一起交流~

The end.

转载于:https://juejin.im/post/5d01a0f3f265da1b957050c6

再谈table组件:固定表头和表列相关推荐

  1. 实现固定表头和表列的table组件

    前端的table在设置overflow后横向.纵向滚动.但数据比较多时,为了查看方便,希望能在纵向滚动时固定表头,在横向滚动时在左边或右边固定特定表列,这是原生不支持的. 目录 实际效果 设计思路 普 ...

  2. vue项目中table表格固定表头和首尾列

    在html中实现table表格固定表头和首尾列的方法和文章很多,本文就不再赘述. 本文主要介绍vue项目中三种情景下实现table表格固定表头和首尾列 情景一:vue+element 只要在el-ta ...

  3. antd 设置表头属性_解决react使用antd table组件固定表头后,表头和表体列不对齐以及配置fixed固定左右侧后行高度不对齐...

    一.固定表头后表体列和表头不对齐 此问题可能在antd3.24.0版本之前都存在,反正3.16.2版本是存在这个问题的,如果是3.24.0之前的版本估计只能通过修改css样式解决. 按照官网说的: 1 ...

  4. antd table动态表头_解决react使用antd table组件固定表头后,表头和表体列不对齐以及配置fixed固定左右侧后行高度不对齐...

    1.固定表头后表体列和表头不对齐 此问题可能在antd3.24.0版本以前都存在,反正3.16.2版本是存在这个问题的,若是是3.24.0以前的版本估计只能经过修改css样式解决.css 按照官网说的 ...

  5. 【css】纯css实现table表格固定表头,表内容滚动

    今天在写公司项目的时候,遇到了一个比较xx的问题,现公司的项目使用的是 vue2.0 和 element-ui 搭建的. 那么提到 element-ui 大家都知道,他最实用的一个组件就是表格了,简直 ...

  6. 微信小程序 table表格 固定表头和首列 右侧表格可以左右滚动(多种表格演练)

    最近在做公司的项目需要到表格展示数据,但是微信小程序是没有原生table标签的,于是就百度各种搜...发现结构有类似的就粘过来修改,要善于学习和借鉴别人的经验使其变成为自己的东西,学无止境~ 在这里做 ...

  7. js固定表头或者表列

    2019独角兽企业重金招聘Python工程师标准>>> <script type="text/javascript">// Minified sour ...

  8. element table固定表头,表的高度自适应解决方法

    element table固定表头,表的高度自适应解决方法 参考文章: (1)element table固定表头,表的高度自适应解决方法 (2)https://www.cnblogs.com/muou ...

  9. php表滑动 其它固定,table固定表头使表单横向滚动

    这次给大家带来table固定表头使表单横向滚动,table固定表头使表单横向滚动的注意事项有哪些,下面就是实战案例,一起来看一下. 1.头部用一个table并用一个p包裹着, 表格的具体内容用一个ta ...

最新文章

  1. 想去Google做AI?面试题在手,全程无忧!
  2. matplotlib中文乱码
  3. api 和 C# 里的接口的区别?
  4. CVPR 2022 57 篇论文分方向整理 + 打包下载|涵盖目标检测、语义分割、人群计数、异常检测等方向
  5. Caffe应用篇----文件格式转换
  6. 访问GitHub超慢的解决办法
  7. 如何使用JavaScript控制台改进工作流程
  8. MediaPlayer 的prepareAsync called in state 8 错误
  9. android 自定义控件 焦点,Android 自定义Button按钮显示样式(正常、按下、获取焦点)...
  10. 2.6 wpf标记扩展
  11. java内存模型—先行发生原则
  12. CCF201609-2 火车购票(100分)
  13. Ubuntu终端里面显示路径名称太长,怎么设置变短【转】
  14. R+大地图时代︱ leaflet/leafletCN 动态、交互式绘制地图(遍地代码图)
  15. C#中各种字符类型的转化
  16. 3. Builder(建造者)
  17. hololens 播放video
  18. java搜索引擎框架_搜索引擎框架介绍
  19. Win300英雄服务器不显示,win10系统玩不了300英雄的还原步骤
  20. Win10系统更新显卡驱动无限蓝屏重启-驱动人生解决方案

热门文章

  1. 花书+吴恩达深度学习(二六)近似推断(EM, 变分推断)
  2. RecyclerView实现多type页面
  3. 第八:Pytes中的fixture大解剖(二)
  4. matlab水蒸气焓值计算_焓变 反应热-化学选修4同步优质系列教案(人教版)
  5. 编写linux脚本操作 java 服务
  6. 如何远程进入linux7.2图形界面,CentOS7.2安装VNC,让Windows远程连接CentOS 7.2 图形化界面...
  7. [Ext JS] 3.3 树(Tree)的定义和使用
  8. [Ext JS] Grid 的复选框行选择之——某些行不能选取
  9. 8.1 Ext JS应用测试概览
  10. android 自定义banner样式_Android中Banner的指示器自定义View