在今年 SEE Conf 之际我们发布了 4.0-rc
版本,茶歇时间,有不少开发者对于 Ant Design 将设计与实现相结合充满了好奇。这次 SEE Conf 没有充足的时间准备相关演讲,因而于此对于 Ant Design 4.0 背后的一些技术实现做一些杂谈。这次就先谈谈 Table。
组合的问题
据统计,Table 是 antd 中高频使用的组件,其丰富的功能充满着排列组合的乐趣。而在日常使用中,我们发现用户会有一些特殊的组合需求。从 api 使用上说,任何属性都是可以组合设置的。但是从实现上来说,总是会一些这样那样的限制导致无法实现。因而在 v3 版本中,我们对组合进行了检查,如果发现不支持的组合通过控制台警告用户。如果你是 antd 的深度用户,你应该已经知道我们聊的是什么属性了:expandedRowRender
和 column.fixed
ref: https://github.com/ant-design/ant-design/issues/9900
在此,我们可以先讲讲 v3 和 v4 中 Table 的固定列实现机制。帮助大家更好的理解:
叠加的 Table
在 v3 中,我们的左侧固定列和右侧固定列分别是两个独立的 Table:
在组件生成时,我们会测量最底层 Table 需要固定的列的宽度,并将其设置到对应的固定列的 Table 上。但是当 Table 内元素存在变化高度时,测量就变得有些困难。会在各种情况下,没有同步到:
ref: https://github.com/ant-design/ant-design/issues/14859
在后续的更新中,我们添加不少检查时机以弥补这种不匹配的情况。但是同时,由于测量这个行为会导致重排(ref: https://developers.google.com/speed/docs/insights/browser-reflow)。它会对 Table 的渲染性能产生影响。我们只能在性能与正确之间做了艰难的平衡。
此外,叠加 Table 也会有一些其他的负面效果。例如通过受控模式打开固定列的 Popup,因为我们渲染了两遍,会出现两个弹出框的问题。此处就不多描述了。
CSS 的救赎
如果你关注过一些 css 的新特性(其实也不太新啦),你就会知道有一个神奇的属性叫做 position: sticky
。它的神奇之处已经有太多文章进行了描述(如果你不太了解,推荐读一下张鑫旭的这篇文章)。 简单来说,它可以以非常低的成本实现元素相对于容器的粘附效果。
在 NG-ZORRO 中,Ant Design Table 早已使用 sticky 进行功能实现。但是由于 v3 版本需要支持旧版的 IE 游览器。我们不得不使用低效而又老旧的实现方式:
而在 v4 版本中,我们将兼容性进行了改动。从原本的 IE 9 ~ 11,改成了 IE 11。你或许会发现,等等 IE 11 还是不支持 sticky
啊!因而我们对 IE 11 采取了降级处理的方式。如果用户使用的是 IE 11,固定列将会被降级成滚动列模式。由于主流游览器,甚至 Edge 都已经开始拥抱未来。我们实在没必要因为 IE 11 让 antd 再等个 99 年。
在使用 sticky
方案后,我们的测量实际从各种渲染阶段简化到了只要在每列的宽度变化时同步一下即可。这里不得不表扬一下 ResizeObserver
同学,它是浏览原生用于监听元素内容尺寸变化的组件。在它成为正式特性之前,已经有了 polyfill 库。我们将其进行了 React 封装:rc-resize-observer ,通过 ResizeObserver
我们获得了更好的性能以及更准确的监听时机。
固定列与展开行的猜想
在解决了上述的性能与同步的问题后,我们开始着手研究如何将固定列与展开行进行组合工作。组合应该是什么效果?想来十分有趣。这是第一版:
同学们纷纷表示,这是什么鬼?固定列遮挡展开行非常的不合理。如果固定列多了,展开行岂不只有一丁点了?于是,我们有了第二版:
同学们表示看的精神分裂了。固定列部分从垂直方向看混合了展开行的一部分,用户如果想看展开行的剩余部分,一滚动上方列也走了。不行不行,于是我们就有了第三版:
嗯,我们终于达成了一致。展开行不应该和固定列产生冲突。展开行作为单独一列,也应该独立于滚动之外。
关于展开
在 antd 的演进过程中,我们希望 antd 能够为使用者带来高效愉悦的工作体验。因此会在细节之处添加一些小巧的设计。这次 Table 重写的过程中。我们对展开按钮进行了调整,对展开按钮添加了一些动画:
ref: https://next.ant.design/components/table-cn/#components-table-demo-expand
此外,眼尖的你或许发现了展开按钮似乎少了一个。在 v3 版本中我们收到不少反馈,希望可以只让部分行可以展开。过去,我们的解决方案是推荐用户自己实现一个展开按钮通过受控方式来进行管理。
在 v3 版本中,我们曾经想通过 expandedRowRender
判断返回内容是否为 null
来决定是否需要支持展开。 但是这会导致鸡生蛋蛋生鸡的问题,从性能考虑,展开行只有在用户点击展开的时候进行渲染。这就导致了我们无法知道是否渲染方法会返回空元素。
在 v4 中,我们添加了 rowExpandable
属性,用于判断是否该行支持展开。 同时,我们也注意到了与展开行相关的属性实在太多了。所以在 v4 版本中,我们将展开相关属性全部收入 expandable
属性中。 并对过去的用法进行了兼容以方便用户升级。
背景色的故事
如果你在 v3 版本中使用过非白色背景,你会发现一个奇怪的现象。似乎只有固定列会有背景颜色,而其余部分缺失透明的:
原因在上面已提及。为了实现固定列效果,我们将多个 Table 进行叠加。为了叠加部分不出现内容重叠,我们对固定列的 Table 设置了背景颜色。
在 v4 版本中,为了支持暗色主题,我们对一些变量进行了抽取。Table 的背景颜色正式提出了一个 @table-bg
变量。因而在 v4 中,Table 会跟随该变量设置背景色,不会再出现背景被透出来的情况。
选择的故事
在 v3 版本中,你或许见过一个叫做 hideDefaultSelections
的属性。 但是多半你不知道这个属性到底会有什么效果。它会在勾选框边上添加一个下拉框并提供两个默认功能:
但是单独开启这个属性并不会有效果,它还有一个条件是你得设置 selections
且至少有一个选项:
这个属性非常的量子"玄"学,你不能单独的使用默认的值,如果你要自定义又得先关掉它。这两个属性中的第一个也略显鸡肋。全选当前也和表头的勾选按钮功能重复,此外用户也无法对操作选项进行排序。
在 v4 版本中,我们将"全选当前页"替换成了"选择全部"。并将他们抽取成 Table 的两个静态属性。便于用户选择与组合:
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
]
排序呢?
v3 的排序我们收到了来自社区的反馈,希望支持多列排序。我们经过考虑,发现多列排序也是一个比较有趣的问题。对于一套数据,哪几列可以同时排序,多列排序以哪列为优先?
因此,我们对 sorter
进行了拓展。它除了支持原本的 function 外,还支持配置:
sorter: {
compare: (a, b) => a.val - b.val,
multiple: 3,
}
其中compare
为原本的比较函数,而新增的 multiple
用于支持多列排序的定义。当设置改值时,此列会被标记成支持多列排序。在支持多列排序的列点击排序会同时生效,其数值则为排序采取的优先级。当用户点击不支持多列排序的列进行排序时,会取消已经排序的列状态。
模板布局
在 Ant Design 4.0 进行时! 一文中,我们曾经想通过 templateAreas
属性简化用户的布局设置:
templateAreas: `
name address address
name building no
`
社区同学们纷纷表示这种写法没有类型支持,不便于使用。从而又提出了新的方案:
const name = ...;
const address = ...;
const building = ...;
const no = ...;
templateAreas: [
[name, address, address],
[name, building, no]
]
这解决了类型定义,但是又会因为 prettier
格式化后失去原本的文本模板布局。考虑再三,废弃了该想法。
总结栏
如果你用 v3 的 Table 遇到了需要一个总结栏的需求,你会发现实现非常痛苦。你需要在 data 中插入一个额外的记录用于存储其值。通过 column 属性自定义渲染样式,颇为复杂。
在 v4 版本中,我们添加了 summary
属性支持。你可以在 Table 中直接渲染一个 tfoot
:
summary
接收一个渲染方法,参数为当前 Table 的数据。因而你可以得到 sortered 和 filtered 之后的数据进行进一步加工。
差不多了~
本来还想写写新版 Table 为了支持虚拟滚动做的一些改动、保持固定标题和固定列对于游览器的一些滚动优化、内部抽象 hooks 等等。考虑一篇写太多,估计大家看的都累。下次有机会再来谈谈吧~
祝好