浅谈多行文本省略

581 阅读8分钟
  • 无聊的话题 + 1

1. 效果

在一些 feed 流页面,尤其是一些社区,需要展示描述的文本信息,但为了保证一屏的展示数量,一般都会用上多行文本省略的展示效果,例如知乎:

Untitled(1).png

这是一个非常常见的功能,大家实际工作中应该也都做过类似的需求,某些场景来说,实现起来也非常简单,但对于某些特定场景,这里面的坑也是有一些的,本人不幸踩了一些,今天就简单分享下其中存在的一些问题。

2 实现

2.1 CSS

如果从纯展示的角度来说,用 CSS 实现是最简单的方式,可以使用以下方法:

.some-class {
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3; /* 行数 */
  -webkit-box-orient: vertical;
}

其中,-webkit-line-clamp 属性指定了最多显示的行数。

兼容性上来说,除了 IE,基本上来说,没什么问题:

Untitled(2).png

但这种展示效果往往是不尽人意的,因为在某些场景,我们还是希望能做到通过某些操作看到全部内容的场景,例如知乎的展开/收缩功能,又或者用户希望文本超过某个行数的时候通过 tooltip 展示完整的内容。当然,这些只通过 CSS 是没法实现的,究其原因,主要是通过 CSS 实现的,没法通过其拿到“溢出”的相关勾子。

摆烂做法,给所有的文本都加上 tooltip 的功能,不管文本有没有溢出。

2.2 JS

既然 CSS 这条路走不通,那只能从 JS 的角度去解决问题。

2.2.1 丑陋版

这个是完全不能用的版本,只是开启个思路。

思路:拿到渲染后的line-height,容器的height ,判断文本是否溢出,即

const { height, lineHeight } = getComputedStyle(node);

const heightVal = pickPXValue(height);
const lineHeightVal = pickPXValue(lineHeight);
// 你需要在 CSS 里设置 lineHieght,因为默认值是 'normal'

const isOverflow = heightVal > lineHeightVal * rows; // rows = 2

setOverflow(isOverflow);

Untitled(3).png

如果检查溢出,就让展示出来,设置相关 class:

.overflow {
  height: 48px; /** lineHeight * rows */
  overflow: hidden;
}

Untitled(4).png

不考虑一些又不是不能用的问题,这种实现存在几个问题

  1. 的位置,理想的状态肯定是跟随着文字的,其次是,如果文本中出现换行,但是这个一直是展示在结尾处 Untitled(5).png
  2. 每次使用都需要设置 line-height,如果不设置,你就拿不到line-height的具体数值,展示就会出现问题

2.2.2 雏形版

雏形版本是我借鉴(抄袭)了某个开源库的版本,因为那个开源库有各种各样的问题。

我们先分析一下丑陋版存在的问题:

  1. 的位置,应该是跟随文本的,即不应该独立于文本之外;
  2. 不需要手动设置一些不必要的属性/样式,比如 line-height
  3. 不够响应式,即容器大小改变的时候,样式会出现问题
  4. 获取不到截断的文本,例如产品希望获得截断文本和用户打开文章关联关系的埋点,那么你就需要获得截断文本,显然上面那种方式是做不到的

问题1和4可以归成一类,只需要能做到 “这是一段很长的文本。这是…” 这样的截断模式,这两个问题都难得到解决。

问题2的解决方式不那么容易,需要一些不那么优雅的方式,即我可以在文本渲染前先手动渲染一个 字符,例如 . ,那么此时容器的 offsetHeight 即文本的 lineHeight

Untitled(6).png

问题3的解决方式比较简单,只需要使用 ResizeObserver 就能解决。

对上述几个问题分析以后,现在主要等待解决的就是找出文本截断的位置,例如以下的文本:

这是一段假设很长的文本。这是一段假设很长的文本。

暴力的方式,直接遍历字符串,就是一个个拼接,过程大概是:

'这...'

'这是...'

'这是一...'

'这是一段...'

// …

即得到一个当前未超出 + 下一个超出高度的结果,那么当前的下标即是我们需要截断的位置,但是这种暴力的遍历方式性能是不好的,打个比方,我的文本很长,截断的长度也比较长,那么这种查找方式性能是比较差的,如何解决问题,那就要用到我们的老朋友“二分法”,相比较暴力遍历,二分法的处理方式就要好很多。

这是一段假设很长的文本。这是一段假设很长的文本。 这是一段假设很长的文本。这是一段假设很长的文本。 这是一段假设很长的文本。这是一段假设很长的文本。 这是一段假设很长的文本。这是一段假设很长的文本。

这样一段文本,截断一行,变化的过程大概是:

Untitled(7).png

因为代码和依赖都不多,就不抽成包发布的形式了,具体源码请查看:

use-clamp-text - CodeSandbox

具体的实现看源码应该是还好理解的,我就说下里面比较 hack 的东西吧,也就是我碰到的一些问题。

  1. 等宽字体的问题,中文是等宽字体的,就是你给文字加粗或者设置斜体,都不会改变布局,但是英文不一样,虽然英文也有等宽字体,但是基本上不会用,你可以尝试给英文加粗或者设置斜体,容器大小就会发生变化,但是不会触发 ResizeObserver 的监听事件。 antd 也把这两个配置单独拎出来了: Untitled(8).png

  2. 经典的 emoji 问题,需要注意切割字符的时候会把 emoji 切分开,展示就会有问题。 Untitled(9).png

  3. 因为需要获取 lineHieght 的,初始会展示一个 . ,页面加载的时候这个点会停留一段时间,且整个二分的变化过程是直接展示出来的,虽然过程很快,但仍然觉得不合理,所以我把二分的状态返回出去了,如果没有结束就把透明度设置成 0 等方式隐藏。

  4. 如果做展开的功能的话,按照我抄袭的思路,是没法统计进去进行计算的,因为算上“展开”是宽度的变化,因为没法很好的把宽度统计进去进行计算(我二分走的是字符串的长度,而不是宽度),所以在 hooks 的参数中加了一个 extra,用和“展开”相同宽度的文字进行占位计算。

2.2.3 进阶版

上面用 hooks 实现的方式看似很方便,但某些问题在 hooks 中是不好解决的:

  1. 我想把获取 lineHeight 和二分的过程和实际渲染隔离开
  2. 前者是通过 innerText 设置文本来计算高度的(原因还是二分时的状态不是有效状态,不应该返回每次二分时切割的字符),这样导致的问题是,“展开”会被冲掉,没法在二分的时候算进布局中,其次是需要设置新的 key 来规避 innerText 的副作用

综合考虑,使用组件的方式似乎更好,对于问题1,可以设置一个 div 元素,fixed 定位,z-index 设置成 -9999,那么整个获取lineHeight 和二分的过程对于用户来说就是不可见的。对于问题二,因为是组件的形式,不像 hooks,整个渲染过程我们是完全可控的,即在二分的时候,“展开/收缩”仍然在布局中中存在,不需要做额外的处理。

于是,我就想到了 antd 的 “排版”:

排版 Typography - Ant Design

我做了一个拷贝,并简单修改了一下:

ellipsis-react - CodeSandbox

其实代码也比较通俗易懂,获取lineHeight 和二分的过程方法就是renderMeasure(’lg’)renderMeasureSlice,看注释的话,刚开始的时候,渲染 ‘l’ 和 ‘g’ 去做顶部以及底部的探测,这里我其实不太理解,我本以为随便找个字符去渲染,然后获取它的offsetHeight 作为行高就可以了,我的 CSS 知识不太行,这里就不做猜测了。

整个过程也还是二分的过程,其实和上面雏形版的逻辑是差不多的,我说一下它是如何解决上述 hooks 中提到的问题的:

  1. 等宽字体的问题不好解决,所以 antd 单独通过 props 进行控制,当你直接修改样式就会这样 Untitled(10).png

  2. emoji 的问题,antd 其实没有做特殊处理,大概乱码展示的宽度和 emoji 的宽度是一样的… Untitled(11).png 渲染: Untitled(12).png

  3. 就是用一个不可见/不可交互的元素进行计算

  4. 如何把“展开”算进去,也是比较简单,组件是 render props 的形式,简单描述就是:

    // 二分的过程,每次计算
    const content = children(text)
    const children = (text) => (
      <span>
            <span>{text}</span>
    	<span>...展开</span
      </span>
    );
    renderMeasure(content) 把"展开"算进去计算是否溢出
    

3. 结语

以上是我在做多行文本省略需求时候的一些思考。

总体上来说,antd 的版本是我觉得比较好的一个,实际上我也是用了这个方式去做多行文本省略,但因为是 C 端,所以只好抄袭了一个版本,去掉一些不必要依赖。

这是我比较早的一个需求,后面觉得整个过程还是蛮有意思,就分享了下,一些细节可能描述的有问题

参考:

  1. github.com/drenther/us…
  2. ant.design/components/…