【每日一面】获取文字的真实宽度

简洁版

代码如下:

typescript 复制代码
/**
 * 创建用于获取文字宽度的 DOM,全局唯一
 * @returns
 */
const createTextDom = (fontSize?: number): HTMLElement => {
  let dom = document.getElementById('get-width-of-text-dom');
  if (dom) {
    if (fontSize) {
      dom.style.setProperty('font-size', fontSize);
    } else {
      dom.style.removeProperty('font-size');
    }
    return dom;
  } else {
    dom = document.createElement('span');
    dom.id = 'get-width-of-text-dom';
    dom.style.display = 'inline-block';
    dom.style.visibility = 'hidden';
    dom.style.zIndex = '-200';
    dom.style.position = 'fixed';
    document.body.append(dom);
    return dom;
  }
};

/**
 * 获取文字的宽度
 * @param text 文本
 * @param fontSize 字体大小
 * @returns
 */
export function getWidthOfText(text: string, fontSize = 16): number {
  const dom = createTextDom(fontSize);
  dom.textContent = text;
  return dom.clientWidth;
}

话多版

为什么需要 "获取文字真实宽度"?

听上去好像没什么用,也很少会遇到问这种问题的面试场景,但现在大厂的面试除了基本的八股外,也在开始搞一些自己的题库用来判断候选人的能力,这种实际就比较灵活化了,无法预测,但也遇到过本文的问题,主要考察两个方面,一是有没有遇到相关的场景,二是前端基本功。

  • 使用场景
  1. canvas 绘图:保证内容相对的居中
  2. 折行问题:Table 组件的表头一行展示完全,出现折行会使表头臃肿(尤其 i18n 场景下,不同语言的文字是不同宽度的)
  • 基本功
  1. DOM 渲染原理
  2. 浏览器样式计算
  3. 边界问题考虑(不同字体、换行、空格)

JavaScript 方案

核心原理

通过隐藏的 DOM 节点来计算文字真实宽度。

避坑指南

  1. 出现计算结果偏差,文字宽度与实际显示的不一致

计算节点(即我们创建的用于计算宽度的 DOM 节点),在字体相关的样式上和目标节点不匹配;计算节点出现了换行,导致宽度被压缩;包含特殊字符(如 emoji)。

  1. 性能问题

频繁创建、删除 dom 节点会触发浏览器频繁的重排和重绘,对于大批量数据展示过程中,页面就会很卡。一般选择复用计算节点+防抖/节流等。

Canvas 特化方案

绘图场景,我们通常很少会直接操作 DOM,尽可能的在 canvas 内部进行处理。

核心原理

canvas 提供了 measureText() 方法直接计算文字的宽度。

代码示例:

javascript 复制代码
// 1. 初始化 canvas,后续我们可以拿到这个 canvas 实例
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const drawText = 'Canvas绘图文字';

// 2. 绘制前,会设置相关的文字属性
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#333';
// 3. 开始绘制前获取到文字宽度用于做一些其他的事情
const textWitdh = ctx.measureText(drawText).width.toFixed(2);
// 4. 假设我们要将文字居中在画布上
const x = (canvas.width - textWidth) / 2;
const y = 50; // 假设居中高度为 50
ctx.fillText(drawText, x, y);

高度的计算还是用 measureText 方法,只是相对比宽度复杂一些

避坑指南

  1. canvas 特化方案不支持富文本的宽度,这种情况需要我们手动进行推算(写计算公式应用)。

面试追问

  1. 问:文字的宽度由什么决定的?浏览器渲染文字的过程。

文字宽度 = 每个字符的宽度之和 + 字符间的间距(letter-spacing),受字体(font-family)、字号(font-size)、字体样式(font-weight/italic) 直接影响;

浏览器渲染文字时,会先根据 font 相关样式从字体文件中读取字符的"advance width"(字符前进宽度,即字符本身宽度 + 默认间距),再累加得到整个文本的宽度,此过程属于"样式计算(Recalculate Style)" 阶段。

一般会由这个文字渲染过度到浏览器渲染的过程等等,这道题本身就是一个简单的出发点,不难。

  1. 问:能否直接用 divoffsetWidth 直接获取?

看实现方式,如果 div没有边距(padding/margin),那么就可以直接获取到文字宽度,如果我们创建的 dom 有默认的边距,记得减掉这部分。此外,还有换行的问题,我们需要配置 white-space: nowrap避免文字换行导致的宽度变小的问题。

  1. 问:你实现的这个版本,可以在任何地方用吗?不行的话,怎么修改?

不行,这个版本并未包含字体相关的属性设置,对于页面多数位置都可以直接使用,但是一些显式的指定了字体属性的,不能直接使用,需要修改。

使用 getComputedStyle来获取目标位置的文字实际样式,同步到用于模拟计算的 DOM 节点,从而保证适配效果。

  1. 问:跳出这个问题,我们在使用 getComputedStyle 做样式同步之后,会触发重排还是重绘?

需要根据同步的样式包含什么样的改动来判断是触发了重排还是重绘。简单举两个例子即可。

  1. 问:还有一个 getBoundingClientRect 方法,你这里怎么不选择这个方法?和 offsetWidth 有什么差别?

前者获取到的时元素内容区域的宽度(不含 padding、border),后者返回的是包含 padding、border 的宽度。不选择这个方法是因为没有必要,我只需要获取宽度即可。

  1. 问:如何计算高度?数据量大的时候如何计算文字宽度?

这个就过于深度的追问了,是另一道比较完全的面试题了。