灏天阁

前端正确处理“文字溢出”的思路

· Yin灏

前端正确处理“文字溢出”的思路

前言: 最近在项目中需要做到类似于 Mac 下这种,当屏幕宽度足以容下当前文件名称的时候,文件名称全部展示,不做省略。
image.png
然而当用户缩放浏览器显示的尺寸时,我们需要做到省略中间的文字,选择保留后缀这种方案。如下图所示:
1.gif

我个人也是感觉这个方案是最好的,因为大部分情况下,用户更关心的是这个文件的类型,而后缀名的保留往往是最佳的选择。我个人也查阅了很多相关文章,并且借鉴了一些已有轮子的代码思路,实现了一个符合我们项目中需求的一个组件。

一.组件效果预览

  1. 单行文字溢出时自动省略,并且不保留后缀。 image.png

  2. 单行文字溢出时自动省略,并且保留后缀。 image.png

  3. 多行文字溢出时,然后再开始省略。这个情况是我们项目中比较特殊的场景。简单来说就是假设我现在想让文字显示两行,如果两行的时候没有溢出,那么正常显示。如果两行情况下还是溢出了,那么我再去处理溢出的文字。 假设这是没有做任何操作的的效果: image.png 使用我们的组件以后的效果: image.png (tips:不一定必须是两行,三行,四行都是可以的。我们接下来实现的组件会让你高度自定义去处理文字溢出的场景。)

  4. 如果你想自己先尝试一下效果,那么你可以快速使用 npm 安装一下。

    • npm i auto-ellipsis-text
    • pnpm i auto-ellipsis-text
    • yarn add auto-ellipsis-text

    原仓库地址: AutoEllipsisTxt 自动省略文字

  5. 使用起来也非常简单,你只需要包裹住你的文字即可 image.png image.png

  6. 话回正题,接下来我会一步一步讲解我实现这个组件的思路,我写的这个组件不一定是最优的,你需要做到知其然并知其所以然,然后完善我写的组件的不足之处,你可以实现自己的自动省略文本方案,才是本文的目的。

二. 单行溢出的处理

  1. 我们先只考虑单行的情况。通常我们在自己的应用中展示很多文件信息的时候,往往选择的布局方式就是高度是一定的,说白了就是高度其实我们是定死的,宽度我们不确定,因为用户有可能会在某些情况下拖动浏览器,造成宽度发生变化,但是总会给宽度一个最小值和一个最大值来保障排版的统一性。
    image.png
  2. 样式方面,在这里我使用的是 UnoCSS ,将样式內联在了标签里,如果你还不了解这种写法,你可以点击下方的文章学习。不过即使你之前从未了解过 UnoCSS ,也不会影响你下面的阅读,因为样式不是本文的重点,并不影响整体阅读。
    手把手教你如何创建一个代码仓库
  3. 让我们先创造一个简单的溢出场景,代码很简单,容器是一个 width 最大值为 200pxheight 为固定 30pxdiv
    image.png
    现在页面上的效果如下图:
    image.png
  4. 可以很清晰的看出,由于我们文字在容器内放不下,但是我们又没对溢出这一特殊场景做出处理,所以就造成了当前页面的效果。先别急,我们一步一步来。
  5. 最开始我去查阅 MDN 的时候,查阅到了一个 “确认过眼神,你就是我要找到人” 的属性。
    image.png
  6. 什么?text-overflow,我们要找到不就是文字溢出时候的处理吗?我兴奋的赶快添加到了我的组件上。
    image.png
    效果如下:
    image.png
    然后看着毫无变化的页面,开始怀疑我自己是不是单词拼错了,然后一个字母字母的比对,排除了单词打错字的情况,但页面还是没有变化。🤔
  7. 于是我又返回 MDN 去查看自己是否遗漏了哪些东西,发现了这样一段文字。
    image.png
    这里直接说结论,其实 text-overflow 这个属性不会为了让文字省略而去创造省略的场景。它其实是在你处理过溢出场景之后,帮你做对于文字溢出的的二次特殊处理。当你对于页面溢出做没有任何操作时,这个属性其实是无效的。 (注意:它仅仅只处理文字溢出的场景。)
  8. 既然你说了,让我们添加额外的属性:overflow-hiddenwhite-space,那么我们就自己添加。我们先只添加一个 overflow-hidden 来看看会发生什么。
    image.png
    我们发现,下面多出去的文字倒是被省略了,但是我们的省略号呢??我就不卖官子了,其实造成这个的原因的答案就是下面这句话:
    image.png
  9. 我们仔细看上面我们溢出的场景。
    image.png
    我们下面两行文字其实是溢出在了盒子下方,正好对应了上面 text-overflow 的介绍,“无法在盒子下面溢出” 这句话。
  10. 在这里我们就需要制造一个让文字强制不换行的场景。那么就需要用到我们另外一个十分重要的属性,white-space
    image.png
    我们本节只需要关系 nowrap 这一个值即可。剩下的值如果读者有兴趣可以自行了解,我们不过多解释。
  11. 首先你要知道,其实我们 web 页面的换行,并不是毫无意义的自己就换行了,而是都有一个隐藏的换行符,你可以把这个隐藏的换行符浅浅的理解为 white-space(空格)
    image.png
  12. 理解了上面那段话,那我们的属性 white-space:nowrap 的中文含义就十分明显了。white-space对应空格no-wrap 代表不换行。连起来的意思就是,遇到空格不换行。而我们的换行其实有一个隐藏的 white-space ,那么我们添加这个属性以后,就会造成一个不会换行的场景。
  13. 让我们先把 text-ellipsisoverflow-hidden 属性删除,只添加 white-space:nowrap 看看页面效果会是怎么样。
    image.png
    效果如下:
    image.png
    可以看到,我们省略了那个隐藏的换行符,所以文字不会自动换行了,那么整段文字都显示到了一行上。此时我们再加上我们的两个属性,overflow-hiddentext-ellipsis,神奇的一幕就发生了。
    image.png
    我们仅仅只使用了几个 CSS 属性就完成了单行情况下不保留后缀的文字溢出处理。

三. 前期准备

  1. 首先你需要准备一个 autoEllipsis.vue 文件,首先写出下面的代码,来和我一起完成这个组件。
<template>
  <div id="autoEllipsisWrapper" ref="container" v-bind="$attrs">
    <span ref="text">
      <slot />
    </span>
  </div>
</template>
  1. 请注意这个 id 叫做 containerdiv 元素将在接下来的内容中起到至关重要的作用。
  2. 接下来使用 ref 分别去拿到这两个 dom 元素。
    image.png
  3. 最后我们需要设计一个函数,在组件挂载以后,让它去正确处理我们文字溢出的场景。
    image.png
  4. 接下来的需求就是,这个 autoEllipsis 函数如何去实现。别着急写代码,我知道你现在有可能还是一头雾水无从下手,让我先带你理清思路然后再开始写代码。

四. 理清思路

  1. 首先我们因为要做到通用性所以, container 的宽度是不能确定的,它的宽度需要根据它外层的父元素来决定,也就是上文中我们提到的有一个最大值最小值宽度的元素。
    image.png
    换句话说,我们这个 container 要去动态的拿到外层父元素的宽度。
  2. 我们先不讲代码如何实现,我们假设现在我们已经拿到了,就叫做 fatherWidth。然后我们再通过刚刚的 ref 获取到的 text dom 元素去拿到外面传进来的文字内容。通过拿到这个 span 元素的 offsetWidth ,就可以拿到文字的长度。通过判断文字的 offsetWidth 是否大于 fatherWidth 。然后我们通过两个宽度相减,可以得出我们到底溢出的文字宽度为多少。
    image.png
  3. 拿到溢出的宽度以后,那么我们就可以用溢出宽度来除以文字大小,(overWidth/fontSize) ,就可以算出我们到底溢出了多少文字。
  4. 假设现在我们现在溢出宽度为 200px。我们的文字大小为 20px,那么 200/20 就算出我们现在溢出了 10 个字。
  5. 我们并且一开始就拿到了总的文字内容,假如我们之前的文字总数为 30 个。那么在这个情况下我们屏幕上只展示了 20 个文字,因为有 10 个字溢出被我们忽略了。
  6. 到这里之后,我们要做的事情就非常简单了,我们只需要从原来 30 个字的中间开始做切割。一边去掉 5 个,那么此时容器恰好可以容下 20 个字。中间我们再手动加上 “…” 省略号不就完美达成了吗?
  7. 上面想表达的意思用大白话来讲,其实也就是去掉中间的10个文字,然后随便再找一个字替换成字符串三个点 ...

五. 完成 autoEllipsis 函数

  1. 第一步就是为了拿到我们放入的文字宽度。注释已经写的很清楚了,就不过多赘述。
    image.png
  2. 然后我们再去拿外面父元素的宽度。此时会出现第一个分支, container 的宽度小于父元素的宽度,很容易可以猜到现在我们的文字内容是完全可以容纳的,不需要做特殊处理。
    image.png
  3. 第二个分支,当我们的 container 宽度大于了父亲元素的宽度,那么我们可以通过传递 props 来区分是否需要保留后缀,如果不需要保留后缀,我们直接给 container设置我们第二个标题讲解的知识就 OK 了。
    image.png

六. 保留后缀的实现

  1. 如果看到这里,你还没有正确的保留后缀思路,我建议你重新去观看一下标题四,这里我们大致的思路就是为了拿到父元素可以容纳多少文字。
    image.png
  2. 这里我们的思路其实就是计算出得出我们需要删除多少个文字
    image.png
  3. 很简单的思路,就是字符串使用 slice 切割我们上面计算得出的,两边需要删除多少文字。
    image.png
  4. 最后的关键一步,我们需要把 containerwhite-space 属性设置为 normal,因为我们已经正确的处理了文字数量,现在的 container 已经不会溢出了。
    image.png

七. 源码

下面是本组件的核心代码 autoEllipsis 函数的源码

function autoEllipsis(container: HTMLElement, textNode: HTMLSpanElement) {
  const str = premitiveText; //1.拿到的所有文字信息
  textNode.textContent = str; //2.将所有文字放入到我们的 span 标签中
  container.style.whiteSpace = "nowrap"; //3.先将文字全部放入到《一行》中,为了计算整体宽度
  container.style.width = "fit-content"; //4. 给 container 设置 fit-content 属性,就可以拿到正确的内容宽度
  const containerWidth = container.clientWidth; //5. 拿到了 container 的宽度

  const parent = container.parentElement; // 拿到外部父元素的宽度
  const parentWidth = parent!.clientWidth || parent!.offsetWidth;
  if (containerWidth <= parentWidth) {
    //如果container 的宽度《小于》父元素的宽度,不做任何处理
    textNode.textContent = str;
    return;
  } else if (cssEntirely.value) {
    container.style.width = parentWidth + "px";
    container.style.whiteSpace = "nowrap";
    container.style.textOverflow = "ellipsis";
    container.style.overflow = "hidden";
    return;
  } else {
    const textWidth = textNode.offsetWidth; //1. 拿到文字节点的宽度
    const strNumer = str.length; //2. 拿到文字的数量
    const avgStrWidth = textWidth / strNumer; //3. 拿到平均每个文字多少宽度
    const canFitStrNumber = Math.floor(
      (parentWidth * props.startEllipsisLine) / avgStrWidth //4. 根据父元素的宽度来计算出可以容纳多少文字
    );

    const shouldDelNumber = strNumer - canFitStrNumber + 1.5; //1. 算出需要删除几个文字(1.5是为了省略号的宽度
    const delEachSide = shouldDelNumber / 2; //2. 因为要保留中间,所以我们不能只从开头删除,也需要从两头删除
    const endLeft = Math.floor(strNumer / 2 - delEachSide); //3. 因为下面要用到 slice 所以需要计算出 index
    const startRight = Math.ceil(strNumer / 2 + delEachSide); //4. 和上面同理

    switch (props.suffix) {
      case true: {
        textNode.textContent =
          str.slice(0, endLeft) + "..." + str.slice(startRight);
        break;
      }
      case false: {
        textNode.textContent = str.slice(0, -shouldDelNumber) + "...";

        break;
      }
    }
    container.style.wordBreak = "break-all";
    container.style.whiteSpace = "normal";
  }
}

八. 优化点

这个组件目前在 ... 省略号的文字占用上,并不能准确的根据文字大小调整所需的字数。也就是下面的 1.5 这个数字无法精确的算出,但是目前我们项目的文字大小是确定的,所以我也就没有再优化了,还希望各位可以提交 Pr 来一起完善这个组件。
image.png

原仓库地址: AutoEllipsisTxt 自动省略文字

- Book Lists -