前言
午后写的这篇文章,微风加阳光,很是惬意🏖。
然后,话不多说,我们直接进入正题,今天主要会讲解一下可伸缩列、固定列、多级表头和几个表格的常见问题✍️,干货满满哦😯。
温馨提示:本篇文章是继上次table 组件了解一下?这篇文章之后写的,所以建议先去阅读一下前面那篇文章👀。
可伸缩列
可伸缩列,顾名思义就是我们可以通过拖动表头的border来实现列宽的大小,看看下面这张图你就懂了👇:
上图的意思应该表达的挺明了,现在我们简要看下具体实现步骤👇:
步:
我们在表头的每个th里面都多增加一个div,也就是上图中红色的部分,然后定位于th的右边即可。
第二步:
在表头header和表体body同级的地方增加一个div来表示上图中的虚线,默认是隐藏的,就像下面这张图:
第三步:
对红色部分的鼠标事件进行监听:当鼠标按下的时候,就显示虚线,并实时改变虚线的left值,当鼠标抬起的时候就隐藏虚线,并计算出拖拽后的列宽,之后修改的columns里面对应列的width即可,这样表头和表体的列宽都会同步改变。当然,还要记得在鼠标抬起时解绑mousemove和mouseup事件,这是个好习惯。
以上就是可伸缩列的实现方式。
固定列
接下来我们来看看固定列是怎么实现的。首先还是 api 的设计,这个应该容易想到,我们在columns里面把需要固定的列添加上fixed属性即可,它的值有两种选择(left或right),就像下面这样:
columns: [
{
title: '姓名',
key: 'name',
width: 100,
fixed: 'left'
}
...
]
为了简化问题,这里我们只考虑左列固定,因为右列固定也是一样的。
这里先一句话说明下原理😁:就是多渲染一份表格并定位在原来的表格之上,下面这张图应该能帮助你理解👇:
其实上图已经是实现的核心了,So,我们就直接说下具体实现方式😎:
步:处理表格数据
因为要把需要固定的列放到左边,所以我们一开始需要处理的就是表格数据,把所有有fixed="left"属性的列排在前面。
第二步:渲染两个表格
再来就是正常渲染两个表格(A 和 B),事实上表格 A 和表格 B 是一模一样的,只不过表格 B多设置了一些属性,比如定位在左上角、定宽(宽度就是有fixed="left"属性的列的宽度之和)、溢出隐藏啊等。具体代码结构如下图所示:
此外,对于表格 A 的fixed部分我们可以设置visibility: hidden,因为它不需要展示,而且 Element 也是这样写的;同样地,对于表格 B 的非fixed部分也可以设置visibility: hidden。
这时候我突然产生了一个疑问🤔,就是为什么要设置成visibility: hidden而不设置成display: none呢?display: none难道不是可以渲染更少的 dom 吗?设置成visibility的意义在哪里?这是个不错的问题,建议大家思考一下下。。。再往下看😁。
带着疑问我顺便看了下 iView 和 Ant Design 的 dom 结构,发现 iView 也是用visibility: hidden来处理的,而 Ant Design 则是直接不渲染了,这就很奇怪啦!于是乎我把 iView 和 Element 的visibility: hidden换成display: none试了一下,发现好像也 ok,表格展示也是对的,没什么问题,所以是为什么呢?其实主要的原因是把visibility: hidden换成display: none会引发行对不齐的问题。
什么意思呢?就是说如果我们设置display: none的话,表格 A 里面的行高是不固定的,但这时候表格 B 是没有展示表格 A 中表体的内容,所以表格 B 不能同步表格 A 里面的高;而如果我们设置成visibility: hidden的话,表格 A 和表格 B 其实是都包含所有数据的,只是视觉上不可见而已,这样它们的行高就能够保持完全一致,虽然会导致多余的 dom 元素。
那 Ant Design 为什么可以呢?其实 Ant Design 也是有这个问题的,虽然它没有渲染多余的 dom,但是它会事先计算出表格 A 的行高,然后在去同步设置固定列的行高,此外在表格大小、列宽变化的时候也要去同步。它们是两种不同的方案,大家自己好好体会一下🙌。
当然,这里我们采用的是 Element 和 iView 的方案。
第三步:同步滚动
上面的实现方式会有什么问题呢🤔?明显的问题就是表格 A 和表格 B 是割裂的,所以滚动其中一个表格的时候,另外一个表格是不会跟着响应的。事实上每个表格里面的表头和表体也是割裂的,所以现在我们要做就是同步滚动:当我们横向滚动表格 A 的表体时,我们需要同步滚动表格 A 的表头部分;当我们纵向滚动表格 A 的表体时,我们需要同步滚动表格 B 的表体部分。
这里以表格 A 里面的表体(A__body)滚动为例子,简要说下具体做法:A__body横向滚动时:获取A__body的scrollLeft值,然后把值同步到表格 A 的表头;A__body纵向滚动时:获取A__body的scrollTop值,然后把值同步到表格 B 的表体。当然滚动是一个高频动作,所以我们可以进行防抖处理;还可以尝试用transform来替代scrollTop和scrollLeft进行滚动。
第四步:同步hover
同第三步一样,当我们hover每一行的时候也需要像滚动一样进行同步,也就是hover样式的同步。
同样地我们以hover表格 A 的表体为例🌰,监听行的mouseenter和mouseleave事件,当鼠标移入到某一行,这时候你是能获取该行信息的,然后同步到固定列的对应行即可;同理hover到固定列的时候也要同步到表格 A,这里就不再赘述。
多级表头
表头
首先还是 api 的设计,我们希望在columns里面加个children就能实现,就像下面这样(顺便看下多级表头长什么样):
columns: [
{
title: '日期',
key: 'date',
width: 200
},
{
title: '配送信息',
children: [
{
title: '姓名',
key: 'name',
width: 200
},
{
title: '地址',
key: 'addr'
}
]
}
]
从上图中我们可以看出多级表头其实是一行一行来渲染的,每行又可以分为一列一列来渲染,实际上就是一个二维数组,两个v-for循环,让我们截个 iView 的代码看看大体结构:
所以我们首先要做的就是对columns进行格式化,使其变成下面这个样子(二维数组):
其中每个数据节点都会给个level、rowspan和colspan,后面两个是用来实现合并单元格的,比如columns的项日期,它的colspan应该是它children的总数,如果没有children就是1;它的rowspan应该等于大层数-当前层数+1,如果有children则为1。
这里可能会有点绕,所以需要停下来理一理思路🤔,但其实本质就是数据结构的转换,所以这里没有特别强调说明如何转换,大家自己动手试一试吧😊。
表体
表体渲染就简单了,只不过我们同样要对columns进行一下处理,我们先看看整理之后的列大概长什么样:
其实新的columns就是遍历一下旧的columns,有children就继续遍历获取子列,没children就直接取出该列,类似于获取到所有叶子节点的列,这个新的列会代替原有的columns来渲染,由于这个数据结构转换简单点,所以我把代码贴了上来:
const getAllColumns = (cols, forTableHead = false) => {
const columns = deepCopy(cols);
const result = [];
columns.forEach((column) => {
if (column.children) {
if (forTableHead) result.push(column);
result.push.apply(result, getAllColumns(column.children, forTableHead));
} else {
result.push(column);
}
});
return result;
};
其实还是个数据转换的问题,大家可以看下下面两张图加深理解👇:
至此,我们就把可伸缩列、固定列和多级表头的实现方式给扯完了,哈哈😁😁😁。
问题补充
1、列宽不一致
事实上我们的表格和表体的列宽其实还是不一致的,比如当表体有滚动条的时候,表头没有,所以就会出现一个滚动条宽度的差距,这时候通常的做法是当纵向滚动条存在的时就在表头末尾加上一列去弥补这个宽度差,一般我们称之为gutter。那这个gutter的宽度值是多少呢?其实就是滚动条的宽度,那如何计算滚动条的宽度呢?大概原理就是创建一个div,然后用(无overflow: scroll属性的div宽度)-(有overflow: scroll的div宽度),就是滚动条的宽度。
2、错位
实际开发中,用 Element 的时候如果表格不知道为啥就错位了,可以尝试用以下方法解决:
this.$nextTick(() => {
this.$refs['table'] && this.$refs['table'].doLayout()
})
3、卡顿
表格里如果满屏都是tooltip和input是会卡顿的,毕竟事件监听和 dom 结构都变多了,所以要避免这种情况的发生。比如行是编辑状态的时候才显示input;tooltip改成实时渲染,不要一开始把每个都加上tooltip。
4、key 值
v-for的时候不要用index来绑定key值,因为index可能是会改变的,所以并不可靠,尽量用id来绑定。
5、大数据渲染
对于成千上万行的数据渲染,如果发生了卡顿是很正常的,这时候如果你的表格数据只是用来纯展示的话可以在组件中加上functional: true使其成为函数化组件,这样能减少渲染开销。另一种就是虚拟列表了,虽说表格数据成千上万,但可视区的数据就那么多、范围就那么大,我们永远只需要渲染可视区域和可视区上下附近的一部分数据即可,其他不用渲染,然后在滚动的时候根据滚动的多少来计算出当前要显示的数据区间即可,大体思路就是这个样子,当然了,It's easier said than done 😂。
小结
吼吼,至此,我们终于把 table 组件的常用功能点讲完了,说实话,是真的麻烦,不过希望能够对你有帮助。后的后,大赞无疆,嘻嘻👍!
作者:尤水就下
链接:https://juejin.im/post/5dbd38f0e51d452a45800dee
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。