用 Canvas 测量文本尺寸

591 阅读5分钟

当前困境

在业务开发时经常需要获取文本大小或者组件渲染后的大小:

  1. 文字超出时显示...,鼠标移入时显示 Tooltip(业务库中 FlexibleText)
  2. 标签列表、多选、级联多选等在表格中需要根据宽度显示动态控制显示数量
  3. 画图业务中需要计算并且存储文本渲染后的实际大小

我们常规的思路:

  1. 先渲染出内容
  2. 基于渲染后的内容获取文本或者组件大小
  3. 基于获取大小做业务处理

这里面有一个非常大的 性能陷阱 ,基于渲染后的元素获取文本大小会触发浏览器的 回流(reflow) ,在一些批量渲染的场景会造成非常大的性能问题,比如表格、列表,因为每一个项渲染的时候都可能触发一次回流这个代价非常大,当然这个问题也有一些解决方案:通过异步的方式获取文本宽高(利用浏览器的缓存能力),如果在一次获取元素宽高后不对元素进行增减,下次在获取其它元素宽高时不会触发回流(reflow),异步的方式可以避免在一个渲染周期内不断循环进行:添加元素、获取宽高的操作,从而避免严重的性能问题。

Canvas 思路

一个另辟蹊径的思路:通过 canvas 的 measureText 测量文本宽度,这个方案看上比直接获取 DOM 元素宽高要复杂一些,因为这个 API 只能测量文本的宽高无法直接测量组件的宽高,需要做一些计算才可以实现更复杂的场景,但它的收益也很明显: 高效、不会触发回流

优点

  1. 不需要先渲染出内容,让业务处理更通顺
  2. 性能卓越
  3. 兼容性较好(不同的浏览器)

缺点

  1. 只能测量文本宽高
  2. 需要做一些计算

测量文本宽度

这块的应用可以参考我们在业务中封装的代码:

defaultFontStyle =
        '12px -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,PingFang SC,Helvetica Neue,Noto Sans,Noto Sans CJK SC,Microsoft Yahei,Arial,Hiragino Sans GB,sans-serif';

initializeCanvas(fontStyle: string) {
  if (!this.context) {
    const canvas = document.createElement('canvas');
    this.context = canvas.getContext('2d');
  }
  this.context.font = fontStyle;
}

measureTextWidth(text, font?: string) {
	font ? (this.context.font = font) : '';
	return this.context.measureText(text).width;
}

可以看出基础代码就几行,唯一麻烦的时需要指定字体、及字体大小,这个会影响测量的准确性。

测量富文本宽高

测量富文本的宽高比上面说的要复杂些,一方面是由于富文本格式会对宽高产生影响,另外一方面文本的高度无法通过 measureText 获取,需要根据计算出来的文本宽度+换行逻辑推算出来。

解决兼容问题:

在做画图业务时其实就需要动态计算文本(支持简单的富文本格式)的宽高,最初的做法就是渲染 DOM 、通过 DOM 测量宽高,因为是编辑中才会触发计算所以性能不是主要问题,但是兼容问题比较明显,一个浏览器中计算出来的宽高在另外一个浏览器上显示时可能因为宽度无法展示全,导出成 PDF 时也会遇到类似的问题,文字显示不全(用了无头浏览器 puppeteer)。

解决时机问题:

在画图业务中使用 DOM 的方式计算文本宽高有一个弊端就是需要等文本完全渲染好了之后可以测量,文本编辑之后需要等一个 Promise/setTimeout 周期才可以计算文本宽度,否则就是不准确的。而使用 canvas 就可以解决这个问题,不需要等待编辑内容完成渲染就可以直接计算宽高,未后续处理可以变得很流畅了。

同时因为使用 canvas 计算宽高不在依赖当前的画板环境,所以计算出来的宽度和高度不受缩放比的影响,也可以有效简化后续的处理流程,同事避免因为缩放比计算带来的精度问题。

文本格式处理

这个比较简单,前面说到测量文本需要字体和字体大小,对于基础文本格式只需要在 canvas 绘制的上下文上增加 bold、italic、动态 fontSize 等属性即可:

const getFont = (
    text: CustomText,
    options: {
        fontSize: number;
        fontFamily: string;
    }
) => {
    return `${text.italic ? 'italic ' : ''} ${text.bold ? 'bold ' : ''} ${text['font-size'] || options.fontSize}px ${options.fontFamily} `;
};
context.font = getFont(data);

高度计算

这块相对复杂一些,因为需要考虑自动换行(文本宽度超过容器宽度)、和主动换行(\n)还要结合 lineHeight(由 fontSize 确定):

基本的公式: 行数 * lineHeight (如果一行的文本存在多个 lineHeight 取最大 lineHeight)

具体的实现行数的计算逻辑就不再贴代码了,因为有不少边界场景,有兴趣的直接参考代码库:

github.com/worktile/pl…

总结

基于 canvas 计算宽高是一个新的思路,以前可能会因为不了解或者担心写起来复杂而没有使用它,但是当我们真正使用它的时候发现收益还是比较明显的,而且计算的复杂度并没有不断地叠加,反而是可以封装起来复用的。

所以当我们遇到比较头疼棘手的问题时还是要多尝试新的思路,不要过多担忧它的复杂度,看起来复杂的东西真正写起来并没有那么复杂。

参考:

github.com/wanglin2/ca…

juejin.cn/post/708377…