一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

一个懂分寸的文本省略组件是怎样炼成的

时间:2026-07-04 11:34:54 编辑:袖梨 来源:一聚教程网


一、问题:tooltip 的"狼来了"困境

先看一个随处可见的写法:

一个"懂分寸"的文本省略组件是怎样炼成的

 复制代码<!-- 常见但粗暴的做法 -->
<el-tooltip :content="text">
  <div class="ellipsis">{{ text }}</div>
</el-tooltip>

这行代码有一个隐蔽的问题:不管文本是否溢出,tooltip 都会出现。用户在"项目管理"四个字上 hover,弹出一个写着"项目管理"的 tooltip——这不仅是废话,更是在消耗用户的注意力。当一个页面上有几十个这样的 tooltip 时,每一次鼠标划过都是一次微小的"狼来了"。

更好笑的是,这个问题在大多数项目中从未被修正,因为:

  • 产品不会提"去掉不必要的 tooltip"这种需求
  • 设计师的 mockup 里看不出 hover 态
  • 测试用例里没有"验证短文本不弹 tooltip"
  • 用起来好像也没什么大问题

于是它就一直在那里,成为页面上一万个微小摩擦中的一个。

一个真正好的省略组件,应该在文本不需要省略时保持沉默。


二、核心判断:浏览器已经算好了,我们只需要读出来

溢出检测的本质不是算法问题,而是一个读值问题。浏览器在渲染文本时已经精确计算了每个元素的尺寸,我们只需要比较两个值:

 复制代码溢出条件:scrollWidth > clientWidth  或  scrollHeight > clientHeight
  • scrollWidth:元素内容实际占据的宽度(包括被 overflow hidden 裁掉的部分)
  • clientWidth:元素可视区的宽度(CSS 盒模型去掉滚动条后的内容宽度)

当内容没有溢出时,scrollWidth === clientWidth,差值恰好为零。

当内容溢出时,scrollWidth 会超出 clientWidth,浏览器裁掉了超出的部分——同时也很诚实地保留了真实宽度的数值。

所以检测溢出不需要任何计算,只需要诚实地问浏览器一句:你的内容有多宽?

 复制代码const checkOverflow = () => {
  const el = overEllipsisRef.value
  if (!el) return
  isOverflow.value = el.scrollWidth > el.clientWidth
                  || el.scrollHeight > el.clientHeight
}

多行省略同理,只不过比较维度从宽度变成了高度:

 复制代码单行省略 → 比较 scrollWidth vs clientWidth
多行省略 → 比较 scrollHeight vs clientHeight(因为 -webkit-line-clamp 裁掉的是高度)

三、不仅是"判断一次":什么时候需要重新判断?

溢出状态不是一个静态值。以下任何情况发生,都可能导致溢出状态改变:

触发场景例子
内容变化数据从接口返回,文本从空变成了一段话
容器尺寸变化用户拖拽侧边栏,表格列宽变了
窗口缩放浏览器从全屏变成半屏
字体/主题切换字体大小变了,或者换了个语言包导致文字变长
行数限制变化从单行省略切换到三行省略

组件对这些场景的覆盖非常细致:

 复制代码// 场景 1:内容变化 → watch content
watch(() => props.content, () => {
  refreshEllipsisState()
})// 场景 2:行数限制变化 → watch line
watch(() => props.line, () => {
  refreshEllipsisState()
})// 场景 3:容器尺寸变化 → ResizeObserver
const initObserver = () => {
  resizeObserver = new ResizeObserver(() => {
    refreshEllipsisState()
  })
  resizeObserver.observe(overEllipsisRef.value)
}// 场景 4:slot 内容更新 → onUpdated
onUpdated(() => {
  refreshEllipsisState()
})

每一种可能导致溢出状态变化的场景,都有一条对应的监听路径。不遗漏触发源,是这个组件可靠性的基础。


四、ResizeObserver 的正确用法

ResizeObserver 是这里最容易被用错的 API。两点值得展开:

1. 不是监听 window,而是监听元素本身

很多开发者习惯用 window.addEventListener('resize', ...) 做响应式,但这有两个问题:

  • 粒度太粗:窗口 resize 不一定影响当前元素(弹窗里的元素、固定宽度的侧边栏不受影响)
  • 漏掉场景:元素尺寸变化不一定伴随窗口 resize——CSS flex/grid 布局中,兄弟元素的增删会导致当前元素尺寸变化,但窗口大小没变

ResizeObserver 直接监听目标元素的尺寸变化,颗粒度精确到像素。

2. 有兜底

代码里有一个值得注意的判断:

 复制代码if (!window.ResizeObserver || !overEllipsisRef.value) return

对于不支持 ResizeObserver 的古董浏览器,组件降级为只在 mount 和 content/watch 变化时检测,不报错、不崩溃。这是一个成本极低的渐进增强——支持了就是更好的体验,不支持也不影响核心功能。


五、把判断结果驱动到 tooltip 的 disabled 属性

这是整个组件最妙的一句模板代码:

 复制代码<ElTooltip :content="tooltipContent" :disabled="!isOverflow">
  <div class="over-ellipsis" ref="overEllipsisRef">
    <slot>{{ props.content }}</slot>
  </div>
</ElTooltip>

disabled 属性在 Element Plus 中控制 tooltip 是否生效。传入 true 时,tooltip 完全不渲染弹出层,不会有 hover 事件监听,不会有 DOM 节点创建。

这意味着:

  • 文本短 → isOverflow = falsedisabled = truetooltip 完全不存在,零开销
  • 文本长 → isOverflow = truedisabled = false → tooltip 正常工作

不是"有 tooltip 但内容为空",而是 tooltip 根本不存在。 这比用 v-if 更优雅——v-if 会销毁重建 DOM,而 disabled 只影响 tooltip 的行为逻辑。Tooltip 的 DOM 结构和事件绑定保持不变,切换开销更小。


六、多行省略的 CSS 配合

单行省略是 CSS 基本功:

 复制代码white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

多行省略需要 -webkit-line-clamp,但注意它要求配合 display: -webkit-box-webkit-box-orient: vertical,这三者缺一不可。组件在这里做了一个干净的属性分离:

 复制代码const isMultiLine = computed(() => Number(props.line) > 1)const ellipsisStyle = computed(() =>
  isMultiLine.value
    ? { WebkitLineClamp: String(props.line) }
    : {}
)
 复制代码<div
  :class="[
    { 'over-ellipsis--multi-line': isMultiLine,
      'over-ellipsis--single-line': !isMultiLine }
  ]"
  :style="ellipsisStyle"
>
 复制代码.over-ellipsis--single-line {
  display: block;
  white-space: nowrap;
}.over-ellipsis--multi-line {
  word-break: break-all;
}

单行用 white-space: nowrap,多行用 -webkit-line-clamp(配合外部的 display: -webkit-box + -webkit-box-orient: vertical),两种省略模式通过 isMultiLine 干净切换。line prop 默认值设为 1,这是一个合理的默认——大多数场景确实是单行省略。


七、完整的心智模型

把这个组件的设计哲学抽象出来,是一条可以迁移的通用原则:

 复制代码┌─────────────────────────────────────────────────┐
│                                                  │
│   状态 X 是否需要展示?                            │
│       ↓                                          │
│   能否在运行时检测?                               │
│       ↓                                          │
│   用浏览器原生 API 读值(不自己算)                  │
│       ↓                                          │
│   识别所有会导致值变化的触发源,逐一监听              │
│       ↓                                          │
│   用 disabled / v-if 控制功能的启用,而非事后补救     │
│                                                  │
└─────────────────────────────────────────────────┘

映射到 OverEllipsis:

步骤实现
状态 Xtooltip 是否需要出现
运行时检测scrollWidth > clientWidth
原生 API 读值直接用 DOM 属性,不自己算字符宽度
触发源覆盖watch content, watch line, ResizeObserver, onUpdated
控制功能启用:disabled="!isOverflow"

这个模式可以复用到很多场景:

  • 截断标记:"查看更多"按钮只在真正截断时才显示
  • 滚动阴影:只在可滚动时在边缘显示渐变阴影
  • 虚拟列表:只在数据量超过阈值时才启用虚拟化
  • 分页器:只在超过一页时才显示分页组件
  • 拖拽手柄:只在列表项 > 1 时才可拖拽排序

八、这个组件也有不完美的地方

实事求是地说,120 行代码不可能完美:

  1. 多行省略的 CSS 依赖外部-webkit-box-orient: vertical 等样式需要在使用侧定义,组件自身没有完整包含
  2. innerText 取 tooltip 内容innerText.trim() 在某些情况下可能和视觉显示的文本有差异(例如包含 ::before 伪元素内容时)
  3. onUpdated 可能触发过于频繁:在频繁更新的场景下(例如动画),每次 VNode 更新都会触发溢出检测,可以加 debounce

但这些都不影响它成为一个设计思路出色的组件。好的组件不是完美的组件,而是在核心问题上给出了清晰且正确的答案的组件。


九、总结

OverEllipsis 的 120 行代码,核心在做一件事:让 UI 的每一个元素都"说实话"

  • 文本真的溢出了,tooltip 才出现
  • 每一条可能导致溢出的变化,都有一条对应的监听
  • 不需要 tooltip 的时候,它彻底不存在,不是"存在但无内容"

这不是炫技,这是一种对用户注意力的尊重。每一个不必要的 tooltip、多余的滚动条阴影、不该出现的"展开更多",都在微量地消耗用户的认知资源。消除这些"微小的不诚实",是前端工程师能够给予产品的最细腻的关怀。


热门栏目