面试官:你是如何获取文本宽度的?

12,515 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

前言

日常开发中,经常会需要获取文本显示宽度来做一些特殊布局, 比如:

  • 文本超过多长时候截断展示省略号...
  • canvas 布局时候在某段文本之后展示特殊标记等

如何才能准确高效的实现获取文本实际的渲染宽度呢? 咱们今天就来一探究竟。

阅读本文,你将获得:

  1. 两种获取方法文本宽度的方法
  2. TextMetrics API

方法一【青铜】

也许最容易想到的方法就是 直接按照当前字体大小 text.length * fontSize

这样简单粗暴,但是仔细想下,文字、字母,标点符号,特殊字符等的出现会让计算有特别大的偏差。

例如: image.png 相同的字符长度差别展示的宽度很明显。

方法二【黄金】

既然要获取的是渲染之后的宽度,那就不妨先在页面上渲染一下,获取一下宽度不就好了!

  1. 创建一个 span 标签,并添加到 body
  2. 设置标签 visibility: hidden
  3. 动态修改 span 的 innerText
  4. dom.offsetWidth 获取其宽度

思路上确实没问题,(笔者也曾经用过此方式获取 canvas 元素的宽度,狗头.gif),但是这种方式的弊端也很明显:

  1. 添加冗余的 dom 元素
  2. 直接操作 dom 性能不佳
  3. 在 vue、react 等存在虚拟 DOM 的情况下,操作真实 dom 的时机通常难以满足逻辑的需要

方法三 【铂金】

TextMetrics api 其实 canvas 上给我们提供了这个专门用来获取文本宽度的 api, 相比方法二要轻松可靠。

使用方式:

  1. 创建一个 canvas 2D 对象
  2. 给 ctx 传入字体和文字大小
  3. ctx.measureText(text) 获取到度量文本对象.width返回宽度

我们基于它封装一下获取文本宽度的方法:

function getActualWidthOfChars(text, options = {}) {
  const { size = 14, family = "Microsoft YaHei" } = options;
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  ctx.font = `${size}px ${family}`;
  return ctx.measureText(text).width;
}

image.png 按照上面的逻辑实现的文本折叠组件却出现了异常情况。

先写个最简用例分析下原因: image.png

在控制台计算的结果: image.png

发现计算出来的长度小于实际的长度。这样一来二去计算就出现了偏差。

为什么会出现这样的偏差呢?

MDN 上有这样的一段描述, 大概的意思是说用actualBoundingBoxLeft + actualBoundingBoxRight的和计算出来的宽度比直接用width获取出来的通常要大一点,但是更准确。原因是slanted/italic文字斜体等原因。 image.png

我们同样的示例对比下两种方式获取到的宽度:

image.png

确实用两个值相加获取到的更大一下,这在一定程度上能保证我们获取的“准确性”, 避免出现上面我们文本超出掉落到下一行的情况。

方法三优化【钻石】

所以我们优化下上面的获取宽度的方法,最终代码如下:

function getActualWidthOfChars(text, options = {}) {
  const { size = 14, family = "Microsoft YaHei" } = options;
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  ctx.font = `${size}px ${family}`;
  const metrics = ctx.measureText(text);
  return Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight);
}

问题

先来看一下下面三组执行结果的对比: image.png

你是否也会和我有相同的疑问: 官方说的两个值相加 通常大于 .width的值,似乎也不是特别牢靠。

我们可以观察出来的规律是,7这个数字会使.width的值变大,超过两数之和。

解决思路

既然有些文本Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight) 更大,有些文本metrics.width更大,通常取较大的值更不容易出错,那么是否可以两者比较取较大值呢?

最终版

function getActualWidthOfChars(text, options = {}) {
  const { size = 14, family = "Microsoft YaHei" } = options;
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  ctx.font = `${size}px ${family}`;
  const metrics = ctx.measureText(text);
  const actual = Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight);
  return Math.max(metrics.width, actual);
}

总结

虽然我们对比了Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight)metrics.width的计算长度的差异,但是真正导致差异的内在原因我们还没能搞清楚,希望后面有机会继续深入研究下。

更文不易,欢迎点赞留言交流,这将成为我写作的动力~