找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2728

积分

0

好友

379

主题
发表于 前天 02:24 | 查看: 6| 回复: 0

最近在项目中需要实现类似Mac系统下的效果:当屏幕宽度足以容纳完整文件名时,全部展示;当用户缩放浏览器导致宽度不足时,则省略中间文字并保留文件后缀。我认为这是最佳方案,因为用户通常更关心文件类型,保留后缀名最符合直觉。

在查阅了大量资料并借鉴已有方案后,我实现了一个满足项目需求的文本省略组件。

一、组件效果预览

  1. 单行文字溢出,不保留后缀。
    单行文字溢出不保留后缀效果

  2. 单行文字溢出,保留后缀。
    单行文字溢出保留后缀效果

  3. 多行文字溢出处理。这是项目中的一个特殊场景。例如,我希望文字最多显示两行。如果两行内没有溢出,则正常显示;如果两行内容依然溢出,则对溢出的文字进行处理。
    未处理时的效果:
    多行文字未处理溢出效果
    使用组件后的效果:
    多行文字处理后效果
    (提示:不仅限于两行,三行、四行均可。接下来实现的组件将允许你高度自定义文本溢出的处理场景。)

如果你想先尝试效果,可以通过 npm 快速安装:

npm i auto-ellipsis-text

pnpm i auto-ellipsis-text

yarn add auto-ellipsis-text

使用起来非常简单,只需用组件包裹你的文本即可:

import { AutoEllipsis } from "auto-ellipsis-text";
<AutoEllipsis suffix="..." :start-ellipsis-line="2">
  这是一个简单的测试,看看文字溢出的情况如何处理
  这是一个简单的测试,看看文字溢出的情况如何处理
</AutoEllipsis>

言归正传,接下来我将逐步讲解实现这个组件的思路。我写的组件不一定是最优解,但希望你能够知其然并知其所以然,然后完善其不足之处,最终实现你自己的自动省略文本方案,这才是本文的目的。

二、单行溢出的处理

我们首先只考虑单行情况。在展示文件列表等场景中,高度通常是固定的,宽度则不确定(用户可能拖动浏览器改变宽度),但总会给宽度设置最小值最大值以保证布局统一。

样式方面,我使用了 UnoCSS 并将样式内联在标签中。不过,即使你不了解 UnoCSS 也不影响阅读,样式并非本文重点。

让我们先创建一个简单的溢出场景。代码很简单:容器是一个最大宽度 200px、固定高度 30pxdiv

<div class="mb-20px text-red"> 下方 div 宽度最大值为 200px,高度为 30 px </div>
<div class="border-solid border-1px border-red max-w-200px h-30px">
  <span class="text-20px">这是一个简单的测试,看看文字溢出的情况如何处理</span>
</div>

页面效果如下:
初始溢出场景

可以清晰看到,由于文字内容超出容器且未对溢出场景做任何处理,导致了当前效果。我们先从基础CSS属性开始探索。

最初查阅 MDN 时,我发现了 text-overflow 这个属性。
MDN text-overflow 说明

text-overflow?我们寻找的不正是文字溢出的处理方式吗?我兴奋地将其添加到组件上。

<div class="border-solid border-1px border-red max-w-200px h-30px text-ellipsis">
  <span class="text-20px">这是一个简单的测试,看看文字溢出的情况如何处理</span>
</div>

效果如下:
添加 text-ellipsis 后效果未变

页面毫无变化。于是我又返回 MDN 查看是否遗漏了什么,发现了这样一段说明:
text-overflow 生效条件

结论是:text-overflow 属性本身并不会强制创建文本省略的场景。它是在你处理了溢出场景之后,对文字溢出进行的二次特殊处理。如果未对溢出做任何操作,这个属性是无效的。(注意:它仅处理文字溢出场景。)

既然要求我们添加 overflow: hiddenwhite-space 属性,那我们就照做。先只添加 overflow: hidden 看看会发生什么。
添加 overflow: hidden 后效果

我们发现,超出的文字被隐藏了,但省略号呢?原因在于下面这句话:
text-overflow 作用方向说明

仔细观察我们的溢出场景:
多行溢出示意图

下面两行文字实际上是向盒子下方溢出的,正好对应了 text-overflow 介绍中 “无法在盒子下方溢出” 的描述。

因此,我们需要制造一个让文字强制不换行的场景。这就用到了另一个关键属性:white-space
CSS white-space 属性示例

本节我们只关注 nowrap 这个值。

首先需要了解,网页中的换行并非无故发生,而是由一个隐藏的换行符控制,你可以将其浅显地理解为 white-space(空白符)。
隐藏换行符示意图

理解了这一点,white-space: nowrap 的含义就十分明确了。white-space 对应空白符no-wrap 代表不换行。连起来就是:遇到空白符不换行。而换行符本身就是一个隐藏的 white-space,添加此属性后,就创建了不换行的场景。

让我们先移除 text-ellipsisoverflow-hidden 属性,只添加 white-space: nowrap 看看效果。

<div class="border-solid border-1px border-red max-w-200px h-30px whitespace-nowrap">
  <span class="text-20px">这是一个简单的测试,看看文字溢出的情况如何处理</span>
</div>

效果如下:
添加 white-space: nowrap 后文字不换行

可以看到,由于忽略了隐藏的换行符,文字不再自动换行,整段内容显示在一行上。此时再加上 overflow: hiddentext-ellipsis 属性,神奇的效果就出现了。
单行省略号效果达成

我们仅用几个 CSS 属性就完成了单行情况下不保留后缀的文字溢出处理。这为我们后续的 JavaScript 动态计算提供了重要的基础思路。

三、前期准备

首先,创建一个 autoEllipsis.vue 文件,并写下以下基础代码:

<template>
  <div id="container">
    <span ref="text">{{ primitiveText }}</span>
  </div>
</template>

请注意,这个 idcontainerdiv 元素在接下来的实现中至关重要。

接着,使用 ref 分别获取这两个 DOM 元素的引用。
获取DOM元素Ref

const container = ref<HTMLDivElement>();
const text = ref<HTMLSpanElement>();

最后,我们需要设计一个函数,在组件挂载后,由它来正确处理文字溢出场景。
定义 autoEllipsis 函数

function autoEllipsis(container: HTMLElement, textNode: HTMLSpanElement) {...}

接下来的核心就是如何实现这个 autoEllipsis 函数。别急着写代码,让我们先理清思路。

四、理清思路

  1. 为了实现通用性,container 的宽度不能写死,它应由其外层的父元素决定(即上文提到的有最大/最小宽度限制的元素)。
    组件结构示意图
    换句话说,我们的 container 需要动态获取外层父元素的宽度。

  2. 假设我们已经拿到了父元素宽度,命名为 fatherWidth。然后,通过之前获取的 text 这个 DOM 元素拿到传入的文本内容。通过获取这个 span 元素的 offsetWidth,就能得到文本的总像素宽度。
    通过判断文本的 offsetWidth 是否大于 fatherWidth,我们可以计算出溢出的像素宽度。
    宽度计算示意图

  3. 得到溢出宽度后,用其除以字体大小 (overWidth / fontSize),就能算出溢出了多少个字。

  4. 假设溢出宽度为 200px,字体大小为 20px,那么 200 / 20 得出溢出了 10 个字。

  5. 假设原文总字数为 30 个。那么此时屏幕上只能完整显示 20 个字,因为有 10 个字因溢出被隐藏。

  6. 接下来就很简单了:我们从原来 30 个字的中间开始切割,左右各去掉 5 个字,这样容器恰好能容下 20 个字。然后在中间手动加上 “...” 省略号,就完美实现了“保留两端”的省略效果。

  7. 用大白话讲,就是去掉中间的 10 个字,并用三个点 ... 字符串替换其中的一部分。

五、完成 autoEllipsis 函数

第一步是获取放入文本的总像素宽度。注释已很清晰,不再赘述。
计算文本总宽度步骤

const str = primitiveText; //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 的宽度

然后获取外部父元素的宽度。此时出现第一个分支:如果 container 的宽度小于等于父元素宽度,说明文字内容完全可以容纳,无需特殊处理。
判断无需处理的场景

const parent = container.parentElement; // 拿到外部父元素的宽度
const parentWidth = parent!.clientWidth || parent!.offsetWidth;
if (containerWidth <= parentWidth) {
    // 如果container 的宽度《小于》父元素的宽度,不做任何处理
    textNode.textContent = str;
    return;
}

第二个分支,当 container 宽度大于父元素宽度时,我们可以通过传递的 props 来区分是否需要保留后缀。如果不需要保留后缀(即单行省略),直接给 container 应用我们在第二部分讲解的CSS属性即可。
使用CSS属性处理单行省略

else if (cssEntirely.value) {
 container.style.width = parentWidth + "px";
 container.style.whiteSpace = "nowrap";
 container.style.textOverflow = "ellipsis";
 container.style.overflow = "hidden";
 return;
}

六、保留后缀的实现

如果你对如何实现“保留后缀”还没有清晰思路,建议重新回顾一下第四部分。核心思路是计算出父元素宽度可以容纳多少个字。
计算可容纳文字数量

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. 和上面同理

思路很简单,就是使用字符串的 slice 方法,根据上面计算出的索引,从两端切割掉相应数量的文字。
使用 slice 切割字符串

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

最后关键一步:将 containerwhite-space 属性设回 normal。因为我们已经正确处理了文字数量,现在的 container 不会再溢出了。
重置 white-space 属性

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

七、核心源码

以下是 autoEllipsis 函数的核心源码:

function autoEllipsis(container: HTMLElement, textNode: HTMLSpanElement) {
  const str = primitiveText; //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 这个数字是经验值,并非精确计算得出。
省略号宽度补偿值

const shouldDelNumber = strNumber - canFitStrNumber + 1.5; //1. 算出需要删除几个文字(1.5 是为了省略号的宽度)

由于我们项目的字体大小是固定的,所以暂未进一步优化。希望各位开发者能提交 PR 一起完善这个 Vue 组件,共同在技术社区中探讨与进步。




上一篇:跨平台键鼠共享指南:免费开源工具实现多设备无缝控制
下一篇:SaaS架构设计中的RBAC权限模型详解:从原理到设计实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:48 , Processed in 0.294763 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表