灏天阁

关于获取文字渲染实际宽度的问题

· Yin灏

背景

业务需要求上要获取文字的宽度,根据这个来测算表单 label 和 input 之间的间距,然后进行 label 左对齐。 image.png

解决方案

怎么获取文字的像素宽度呢,有几种方案。

  1. 直接测算一个文字大约宽度是多少,方法比较简单,能满足表单类 90%的场景,比如
export function strCodeLen(str: string) {
  let count = 0;
  if (str) {
    const len = str.length;
    for (let i = 0; i < len; i++) {
      // 字符编码大于255,说明是双字节字符(即是中文)
      if (str.charCodeAt(i) > 255) {
        count += 2;
      } else {
        count++;
      }
    }
    return count;
  } else {
    return 0;
  }
}
// 这个是看单个字符的宽度
console.log(strCodeLen("用户名") * 9);
  1. 用真实的 dom 做渲染,然后获取 dom 的宽度,计算得到最终的准确的字符像素长度。
function getStrWidth(arr) {
  const labelSpan = document.createElement("span");
  const container = document.body;
  let maxWidth = 0;
  if (arr.length > 0) {
    labelSpan.style.width = "auto";
    labelSpan.style.position = "absolute";
    labelSpan.style.whiteSpace = "nowrap";
    container.appendChild(labelSpan);
    arr.forEach((str) => {
      labelSpan.innerHTML = str;
      maxWidth = Math.max(labelSpan.offsetWidth, maxWidth);
    });
  }
  container.removeChild(labelSpan);
  return maxWidth;
}
  1. 使用 canvas 来绘制每个文字大小
function getLabelWidthCanvas(arr, defaultFont) {
    // 创建一个全局canvas元素
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d') as unknown as CanvasRenderingContext2D;
    function getPageFont() {
        // 获取<html>元素的样式
        const htmlStyles = window.getComputedStyle(document.documentElement);
        // 获取font-family属性值
        const fontFamily = htmlStyles.getPropertyValue('font-family');
        return fontFamily;
    }
    const font = defaultFont || `12px $ {
        getPageFont()
    }`;
    let maxWidth = 0;
    if (arr.length > 0) {
        arr.forEach(str = >{
            const width = getStringPixelWidth(str, font);
            maxWidth = Math.max(width, maxWidth);
        });
    }
    return maxWidth;
    function getStringPixelWidth(text: string, font: string) {
        // 设置字体样式
        context.font = font;
        // 使用measureText方法获取文本宽度
        const metrics = context.measureText(text);
        // 返回文本宽度,即像素长度
        return metrics.width;
    }
}

方案对比

第一种方案:没有考虑一些特殊字符,字体大小和字体类型,简单粗暴,没有考虑兼容性,最后会遇上各种问题。

第二种方案:没有第一种方案的问题,但是数据量大,页面会不停的回流,毕竟 dom 元素内容在变化,有性能问题。

第三种方案:会有第一种方案的字体大小问题,需要手动设置字体大小,但是不存在 dom 渲染性能问题,直接存储在内存中。

canvas 和 span 时间对比

// Canvas method
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
function getStringPixelWidthCanvas(text, font) {
  context.font = font;
  const metrics = context.measureText(text);
  return metrics.width;
}
// Span method
function getStringPixelWidthSpan(text, font) {
  const span = document.createElement("span");
  document.body.appendChild(span);
  span.style.position = "absolute";
  span.style.whiteSpace = "nowrap";
  span.style.font = font;
  span.textContent = text;
  const width = span.offsetWidth;
  document.body.removeChild(span);
  return width;
}
// Test data
const textArray = [
  "你好,世界!",
  "Hello, World!",
  "こんにちは、世界!",
  "안녕하세요, 세상!",
];
const font = "16px Arial";
// Performance test
function testPerformance(methodName) {
  const startTime = performance.now();
  for (let i = 0; i < 1000; i++) {
    textArray.forEach((text) => {
      if (methodName === "canvas") {
        getStringPixelWidthCanvas(text, font);
      } else if (methodName === "span") {
        getStringPixelWidthSpan(text, font);
      }
    });
  }
  // document.body.removeChild(span);
  const endTime = performance.now();
  const duration = endTime - startTime;
  console.log(`${methodName} method took ${duration.toFixed(2)} milliseconds.`);
}
// Run performance tests
testPerformance("canvas");
testPerformance("span");

数据结果

// 10 个时间对比
canvas method took 0.20 milliseconds.
span method took 8.00 milliseconds.

// 1000 个时间对比
canvas method took 10.50 milliseconds.
span method took 375.50 milliseconds.

结论

canvas 的效果目前看是最好的,推荐使用 canvas 来解决这个问题

- Book Lists -