CSS Grid代替table元素实现全功能表格的可行性与优势

1,298 阅读6分钟

前言

表格作为一个组件库最复杂的组件之一,我一直在想如何实现会更优雅高效。
HTML table 元素天然就限制了 DOM 结构,我们必须要以行的维度渲染,在行中渲染每一列的单元格。我们日常用的表格组件一般是 columns 属性加 data 的方式,即使支持用 jsx column 作为children,那些 column 也只是用来收集列信息,实际并不会渲染内容。

由于我要做的表格组件是基于Custom Elements,必须要支持以 DOM children 的方式渲染列,如果 children 只是收集列信息会十分低效,这大概也是市面上基于自定义元素的组件库很少提供全功能表格的原因了。

但是,我想到了CSS Grid。Grid 天然就很适合用于实现表格,其还能带来很多 table 不支持的特性。在上个月的探索实现后,我发现完全是可行的。接下来我来带大家解析下如何利用 Grid 来实现表格的常见功能,我们先来看看表格有哪些功能与内容和布局有关:

  • 基础的表格头和单元格渲染
  • 自定义行高,列宽
  • 单元格对齐方式
  • 嵌套列的支持
  • 列隐藏
  • 表头合并,单元格行列合并
  • 粘性列,滚动时粘在左侧或右侧
  • 行展开内容的支持,树形数据渲染支持

接下来一一解析

基础渲染

由于 Grid 支持通过grid-auto-flow: column来按列自动排布,我们完全可以将渲染完全交由列组件,列组件自行按顺序将表头和数据依次渲染出来,自然就排列成了我们想要的表格。与传统表格不同,这样的渲染就没有了行的概念,只有单元格,也大大减少了 DOM 的数量。

在表格根元素上,我们只需按照data的数量生成grid-template-rows,根据列的数量生成grid-template-column,自然就可以自定义每一行和每一列的宽高。而且由于 Grid 本身的强大,宽高还可以指定1fr, fit-content(), minmax(min, max)这样的值,使用fr单位还可以享受灵活的动画过渡

至于单元格的对齐,虽然单元格可以通过justify-selfalign-self更改其对齐方式,但这样会导致其背景异常,单元格本身应该是填充本格的。我们可以给单元格设置 flex,用 flex 去实现对齐。

表头合并,单元格行列合并

利用grid-columngrid-row设置左上角单元格的 span 让其占据其他单元格的空间,被覆盖的单元格设置display: none,这样便可轻易实现单元格合并,其他的单元格仍能够根据顺序正确渲染

QQ图片20241231202201.png

嵌套列(表头分组)

嵌套列.png

要实现这样的嵌套列,用table元素会很麻烦。而对于 Grid,由于我们本身就是按列进行渲染,只需处理一下所有列获得两个信息:有几层嵌套以及列下面有几个根节点列,然后从上到下依次渲染即可

以上图为例,GrandParent下面有两层嵌套,因此表头需要3行,其他非嵌套的列只需给表头设置grid-row: span 3即可。而对于嵌套列 GrandParent,其下面有四个根节点列,需要给其设置表头设置grid-column: span 4,对应地 Parent1 和 Parent2 设置span 2即可。对于非根节点列,其只需渲染表头,其他内容交由 children 渲染即可,因此 age列 正常渲染表头和行数据,然后就结束了,从上到下完美排版!

列隐藏

table元素中,如果要隐藏列只能选择在每一列都不渲染对应列的单元格。而在 Grid 中,我们可以通过将宽度改为0fr来实现隐藏,利用fr还可以带来过渡效果,这是一般方式难以做到的

动态隐藏列.gif

可展开行

table要实现可展开行,只需在那行的下面再渲染一个tr,并将tdcolspan设置为表格列的数量即可。但很显然,这样是不好做过渡的,我们可以看到antd的表格展开行都是直接突兀展示出来。

antd展开行.gif

对于 Grid,要想单元格占据整行,可以将grid-column设置为1/-1span 列数量,要注意这两种方式是有区别的,如下图所示:

grid expand row.png

我们可以看到,1/-1会使该单元格脱离自动布局而优先排列,这会导致其出现在其他单元格前面,要想正常我们还需要手动设置grid-row索引使其到正常位置;如果是span则仍然是自动布局,我们只需在第一列的对应位置渲染展开行的内容即可

至于过渡效果,Grid显然可以通过fr非常简单实现

lun展开行.gif

至于渲染树形数据,我还没有实现该功能,但相信思路和展开行是一样的

粘性列

粘性列在 table 和 Grid 都需要通过position: sticky实现。第一列和最后一列只需设置leftright为0,但由于 DOM 结构的限制,后面的粘性列需要计算前面粘性列的宽度才能设置正确的leftright,这样才能多个列堆叠粘在一侧。因此,在未显式指定列宽度时需要手动计算,并设置ResizeObserver监听才能正常工作。

然而,Grid 有一个表格做不到的特性:Subgrid。子网格可以让其下面的元素使用父网格的布局,从而避免了 DOM 结构的限制。因此如果所有粘性列是紧挨在一侧的,它们完全可以添加一个 Grid 父元素,在该父元素上设置grid-template: subgrid/subgrid,并给其设置position: stickyleft: 0,其下面所有列即可按原来的方式渲染,全部都自动成为了粘性列,无需再手动计算宽度!遗憾的是,Subgrid 有兼容性要求,暂时只能做成可选功能,当检测到不支持时可以按原来的方式实现。

总结

可以看到,CSS Grid 对于这些表格功能可以完美支持,且实现起来也比 table 元素简单。据说 Grid 的渲染效率也比 table 高,不过这个就有点难验证了。

至于其他表格功能如单选、多选、虚拟渲染、排序、分页,它们对布局没有要求,这里就不过多介绍了。

那 Grid 有什么缺点吗,当然有。Grid 的可访问性天生不如 table,但我们可以通过手动加 role、aria-rowindex等属性去支持这些。另外 table 支持边框合并,这个在 Grid 是没有的

上述表格功能均可以在我做的这个组件库网站查看,可以去实时编辑查看效果,有什么建议或疑问欢迎随时跟我提。源码则可以在Github查看,喜欢的话可以给个 Star 哦,该组件库我还会持续开发下去,其更多介绍可参考我上一篇文章

最后,祝大家新年快乐~