探索el-table列宽自适应内容的实现

14,814 阅读5分钟

最近项目上线之后,产品对首页的列表提出了列宽自适应内容的需求,即列的宽度由当前列中内容最宽的一个单元格决定(单元格内容不换行)。项目中的表格使用的是element-ui的table组件,非常强大,满足了各种各样的需求,例如固定列,固定表头,展开行等等。但是面对这样的需求,貌似并没有直接提供配置项支持。

思路一:Fit属性

最早接到这个需求的第一反应是想到element文档中提到的fit属性。在仔细查阅了文档之后,发现这个属性是默认开启的,实际上fit的准确含义是所有列撑满整个表格所在的容器。fit属性的相关源码如下:

    //没有指定宽度的列
    let flexColumns = flattenColumns.filter((column) => typeof column.width !== 'number');
    const fit = this.fit;
    const bodyWidth = this.table.$el.clientWidth;
    let bodyMinWidth = 0;
    if (flexColumns.length > 0 && fit) {
      //获取表格的最小宽度
      flattenColumns.forEach((column) => {
        bodyMinWidth += column.width || column.minWidth || 80;
      });

      const scrollYWidth = this.scrollY ? this.gutterWidth : 0;
      //如果最小宽度小于容器宽度,即没有撑满容器
      if (bodyMinWidth <= bodyWidth - scrollYWidth) {
        this.scrollX = false;

        const totalFlexWidth = bodyWidth - scrollYWidth - bodyMinWidth;
        //把富余的宽度均分给除第一列的其他列,剩下来的给第一列(避免宽度均分的时候除不尽)
        if (flexColumns.length === 1) {
          flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth;
        } else {
          const allColumnsWidth = flexColumns.reduce((prev, column) => prev + (column.minWidth || 80), 0);
          const flexWidthPerPixel = totalFlexWidth / allColumnsWidth;
          let noneFirstWidth = 0;

          flexColumns.forEach((column, index) => {
            if (index === 0) return;
            const flexWidth = Math.floor((column.minWidth || 80) * flexWidthPerPixel);
            noneFirstWidth += flexWidth;
            column.realWidth = (column.minWidth || 80) + flexWidth;
          });

          flexColumns[0].realWidth = (flexColumns[0].minWidth || 80) + totalFlexWidth - noneFirstWidth;
        }
      }else{
          //fit=false,对没有设置宽度的列宽度设为80
          flattenColumns.forEach((column) => {
            if (!column.width && !column.minWidth) {
            column.realWidth = 80;
            } else {
            column.realWidth = column.width || column.minWidth;
            }
            bodyMinWidth += column.realWidth;
          });      
      }

从源码上可见这个属性并不是我们想要的列宽自适应内容,这段代码也是updateColumnsWidth的主要逻辑。总结一下,列的宽度和内容在el-table内部并没有联系。因此,想要列宽自适应内容只能从外部着手。

思路二:table-layout:auto

既然el-table没有配置项支持,那能否从css角度解决呢?我联想到原生table就有这样的功能,简单介绍下原生table的布局模式。

table-layout CSS属性定义了用于布局表格单元格,行和列的算法

auto 大多数浏览器采用自动表格布局算法对表格布局。表格及单元格的宽度取决于其包含的内容。

fixed 表格和列的宽度通过表格的宽度来设置,某一列的宽度仅由该列首行的单元格决定。在当前列中,该单元格所在行之后的行并不会影响整个列宽。 使用 “fixed” 布局方式时,整个表格可以在其首行被下载后就被解析和渲染。这样对于 “automatic” 自动布局方式来说可以加速渲染,但是其后的单元格内容并不会自适应当前列宽。任何一个包含溢出内容的单元格可以使用 overflow 属性控制是否允许内容溢出。

el-table采用的就是第二种table-layout:fixed模式,能否通过改变table-layout为auto来实现呢?尝试之后发现是行不通的。一个原因在于,el-table的固定表头和固定列都需要生成额外的table来实现。这样一来控制多个table之间的列宽同步就成了问题。而解决这个问题的原理就是:为 <table>增加 <col>元素,在 <col>元素上设置 width 属性,动态的同步表格头和表格内容的<col> 元素上的宽度设置。这个方案恰恰是建立在table-layout:fixed的前提之下的。

详情请戳饿了么团队大佬的文章

如果你项目中对表格的需求仅停留在展示,其实是可以考虑使用原生的table的,使用非fixed模式的table非常简单的实现列宽自适应内容的需求。(产品提出该需求参考的头条广告后台,就是使用的原生table,但是也有很多缺点)

思路三:手动计算列宽

'自动'的路子看来是行不通了,那只能在渲染之前手动算出列宽了。对于内容是文本的列,这么做是可以的。把文本的字符类型分为三种:中英文,数字,特殊字符,通过正则筛选出来。再对每个类型的字符乘以字体大小再折合一定的比例,就得到了大致的宽度。循环整组表格数据得到需要自适应内容的列宽的最大值,取其和表头宽度之间的最大值即可。

    getMaxLength (arr) {
      return arr.reduce((acc, item) => {
        if (item) {
          const str = item.toString()
          const char = str.match(/[\u2E80-\u9FFF]/g)
          const charLen = char ? char.length : 0
          const num = str.match(/\d|\./g)
          const numLen = num ? num.length : 0
          const otherLen = str.length - charLength - numLength
          let calcLen = charLen * 1.1 + numLen * 0.65 + otherLen * 0.5
          
          if (acc < calcLen) {
            acc = calcLen
          }
        }
        return acc
      }, 0)
    }

即使是对于文本类型的列而言,上述计算的得到的宽度也并非精准的宽度。因为字体分为等宽字体和比例字体,web上大部分的中文字体都是等宽字体,比如用的最多的微软雅黑。但是英文字体基本都是比例字体了,每个字母的宽度是不同的。

总结

由于目前项目中的自定义列宽度都是可以预先知道的,例如开关,下拉选择框等。所以对文本类型的列做一个手动宽度计算已经基本可以满足产品提出的需求。研究需求的过程中,在网上找到一个开源的列宽自适应组件,也是基于el-table的二次封装,如果各位想直接拿来用的话可以参考。

传送门

勉强算是解决了问题,但是显然不是一个完善的方案,如果各位有好的建议或者实现,欢迎评论告知。