一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
前言
日常开发中,经常会需要获取文本显示宽度来做一些特殊布局, 比如:
- 文本超过多长时候截断展示省略号...
- canvas 布局时候在某段文本之后展示特殊标记等
如何才能准确高效的实现获取文本实际的渲染宽度呢? 咱们今天就来一探究竟。
阅读本文,你将获得:
- 两种获取方法文本宽度的方法
- TextMetrics API
方法一【青铜】
也许最容易想到的方法就是 直接按照当前字体大小 text.length * fontSize
这样简单粗暴,但是仔细想下,文字、字母,标点符号,特殊字符等的出现会让计算有特别大的偏差。
例如:
相同的字符长度差别展示的宽度很明显。
方法二【黄金】
既然要获取的是渲染之后的宽度,那就不妨先在页面上渲染一下,获取一下宽度不就好了!
- 创建一个 span 标签,并添加到 body
- 设置标签
visibility: hidden
- 动态修改 span 的 innerText
dom.offsetWidth
获取其宽度
思路上确实没问题,(笔者也曾经用过此方式获取 canvas 元素的宽度,狗头.gif),但是这种方式的弊端也很明显:
- 添加冗余的 dom 元素
- 直接操作 dom 性能不佳
- 在 vue、react 等存在虚拟 DOM 的情况下,操作真实 dom 的时机通常难以满足逻辑的需要
方法三 【铂金】
TextMetrics api 其实 canvas 上给我们提供了这个专门用来获取文本宽度的 api, 相比方法二要轻松可靠。
使用方式:
- 创建一个 canvas 2D 对象
- 给 ctx 传入字体和文字大小
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;
}
按照上面的逻辑实现的文本折叠组件却出现了异常情况。
先写个最简用例分析下原因:
在控制台计算的结果:
发现计算出来的长度小于实际的长度。这样一来二去计算就出现了偏差。
为什么会出现这样的偏差呢?
MDN 上有这样的一段描述, 大概的意思是说用actualBoundingBoxLeft + actualBoundingBoxRight
的和计算出来的宽度比直接用width
获取出来的通常要大一点,但是更准确。原因是slanted/italic
文字斜体等原因。
我们同样的示例对比下两种方式获取到的宽度:
确实用两个值相加获取到的更大一下,这在一定程度上能保证我们获取的“准确性”, 避免出现上面我们文本超出掉落到下一行的情况。
方法三优化【钻石】
所以我们优化下上面的获取宽度的方法,最终代码如下:
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);
}
问题
先来看一下下面三组执行结果的对比:
你是否也会和我有相同的疑问: 官方说的两个值相加 通常大于 .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
的计算长度的差异,但是真正导致差异的内在原因我们还没能搞清楚,希望后面有机会继续深入研究下。
更文不易,欢迎点赞留言交流,这将成为我写作的动力~