前端打工人必看:用 measureText 精准拿捏 Canvas 文字宽度(附避
前端打工人必看:用 measureText 精准拿捏 Canvas 文字宽度(附避坑指南)
为啥画个文字还要量尺寸?别急,等你被 UI 设计师追着改对齐就懂了
前阵子公司做活动页,产品经理甩过来一张设计稿:「就按这个来,文字要居中,气泡要自适应,还要支持换行。」我瞅了一眼,心想这不就是画个 Canvas 吗,分分钟搞定。结果真正上手才发现,文字这玩意儿在 Canvas 里就是个熊孩子,表面上乖乖站着,实际上到处乱跑。
你以为 ctx.fillText('你好世界', x, y) 就完事了?天真!文字压根不听你的指挥,左边多出来一截,右边又空着一大块,设计师看了直摇头:「这不对齐啊,重做!」那一刻我才深刻体会到,Canvas 里画文字,不量尺寸就是给自己挖坑。
Canvas 里文字不是你想放就能放
在 DOM 世界里,文字天生带盒子。div 有 clientWidth,span 有 getBoundingClientRect(),你随时能知道它占多大地方。可 Canvas 是一片白纸,文字画上去就像泼出去的水,浏览器不会告诉你「这段字 120px 宽、28px 高」。于是你只能凭感觉盲调 x、y,结果一跑真机,字体渲染差异直接教你做人。
更惨的是,一旦涉及多语言、emoji、动态字号,翻车率呈指数级上升。你以为居中就是 (画布宽 - 文字宽) / 2?实际上文字宽到底是多少,鬼知道!直到我遇见 measureText,才发现原来浏览器偷偷给咱们留了一把尺子,只是很多人不知道咋用。
measureText 是啥玩意儿
一句话:这是 Canvas 2D 上下文自带的「文字卷尺」,调用一次返回一个对象,里面装着这段文字在当前字体下的各种尺寸信息。零依赖、零配置,浏览器原生支持,从 IE9 到现在全员在线。
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = '16px sans-serif';
const measure = ctx.measureText('hello 世界');
console.log(measure.width); // 82.34 单位是 px,精确到小数点后两位
看到那个 width 没?它就是这段文字在画布上的横向占地面积。有了它,你就能算出居中位置、换行断点、气泡拉伸长度,再也不用瞪眼猜。
这 API 到底怎么用
最简姿势一行代码:
const textWidth = ctx.measureText('前端真好玩').width;
但真实项目里,字体、字号、字重、行高一个都不能少。先把 font 属性写全,再测:
ctx.font = 'bold 24px/1.5 "PingFang SC", "Helvetica Neue", sans-serif';
const m = ctx.measureText('iPhone 14 Pro 只要 9999 元');
console.log(m.width); // 261.45
注意:
font的语法和 CSS 完全一致,顺序不能乱。- 如果字体列表里有自定义字体,确保已经加载完成,否则拿到的是 fallback 字体的宽度。
- 测量之前一定要
ctx.font = ...先赋值,否则默认是10px sans-serif,数据直接跑偏。
别被 width 迷了眼
很多人以为 measureText 就返回一个数字,其实它是一个对象,除了 width 还有一堆冷门属性,关键时刻能救命:
const metrics = ctx.measureText('中文 ABC');
console.log(metrics);
/*
{
width: 104.67,
actualBoundingBoxLeft: -3, // 字形最左边相对于文本原点的偏移,负值表示向左探出
actualBoundingBoxRight: 101, // 字形最右边相对于文本原点的偏移
actualBoundingBoxAscent: 22, // 字形最高点到基线的距离
actualBoundingBoxDescent: 6, // 字形最低点到基线的距离(基线以下为正)
fontBoundingBoxAscent: 27, // 字体设计时 ascent,不受具体字形影响
fontBoundingBoxDescent: 8,
emHeightAscent: 21,
emHeightDescent: 5
}
*/
做垂直居中时,如果你只按 fontSize/2 去算,往往偏上或偏下。正确姿势是用 actualBoundingBoxAscent 和 actualBoundingBoxDescent 算出真实高度:
function drawTextVerticalCenter(ctx, text, x, centerY) {
const m = ctx.measureText(text);
const realHeight = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent;
const baselineY = centerY - (realHeight / 2) + m.actualBoundingBoxAscent;
ctx.fillText(text, x, baselineY);
}
同理,如果你要给文字加背景色块,用 actualBoundingBoxLeft/Right 就能避免「左右留白不一致」的尴尬。
字体一换,世界崩塌
系统字体通常秒加载,但项目里不用个自定义字体都不好意思说自己是做品牌的。于是你开开心心 @font-face 引入「阿里巴巴普惠体」,结果 measureText 拿到的宽度永远比真实渲染窄一截,why?
因为 measureText 测的是「当前已生效」字体。自定义字体文件还没下载完,浏览器会用 fallback 字体先顶着,你这时候测,拿到的就是 fallback 的宽度。等字体加载完再测,数据才对得上。所以记住两条铁律:
- 等字体就绪再测:
document.fonts.ready.then(() => {
ctx.font = '20px "AlibabaPuHuiTi"';
const w = ctx.measureText('双 11 爆款清单').width;
drawPoster(w);
});
- 如果字体可能动态切换,把测量逻辑包在
document.fonts.load()里:
await document.fonts.load('20px "AlibabaPuHuiTi"');
ctx.font = '20px "AlibabaPuHuiTi"';
// 现在测,稳了
高 DPI 屏幕下的玄学问题
Retina 屏上,Canvas 默认被浏览器放大 2 倍绘制,结果文字模糊得像毛玻璃,测量值也跟着飘。解决方案两步走:
- 按
devicePixelRatio放大画布实际尺寸:
const dpr = window.devicePixelRatio || 1;
const canvas = document.querySelector('#myCanvas');
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
- 所有坐标、尺寸用 CSS 像素做单位,放大交给
scale,measureText返回的宽度已经是 CSS 像素,无需再除dpr。
一顿操作后,文字立马高清,测量值和渲染像素完美对齐,设计师再也挑不出刺。
性能别乱造
有人担心「频繁调 measureText 会不会卡?」我专门跑了个测试:MacBook M1 上循环 10 万次测一段 20 个字符的字符串,耗时 20ms 左右,平均每次 0.2μs。结论:它比你想象中快得多。但别作死:
- 不要在
requestAnimationFrame里每帧测几十段长文,再快也扛不住。 - 缓存!同样的文字、同样的字体,测一次存起来,用
Map或对象做键值映射:
const measureCache = new Map();
function cachedMeasure(ctx, text, font) {
const key = `${font}||${text}`;
if (measureCache.has(key)) return measureCache.get(key);
const oldFont = ctx.font;
ctx.font = font;
const m = ctx.measureText(text);
ctx.font = oldFont;
measureCache.set(key, m);
return m;
}
实战场景拉满
动态生成海报时自动换行
需求:用户昵称最长 20 字符,超出自动换行,气泡宽度固定 300px。
思路:逐个字符累加宽度,超了就在上一个安全断点截断。
function breakLines(ctx, text, maxWidth) {
const chars = Array.from(text); // 正确处理 emoji
let line = '';
let lines = [];
for (let c of chars) {
const testLine = line + c;
const { width } = ctx.measureText(testLine);
if (width > maxWidth && line !== '') {
lines.push(line);
line = c;
} else {
line = testLine;
}
}
lines.push(line);
return lines;
}
// 使用
ctx.font = '22px "PingFang SC"';
const nickname = '超级无敌暴龙战士🦖';
const rows = breakLines(ctx, nickname, 300);
rows.forEach((row, i) => {
ctx.fillText(row, 50, 100 + i * 30);
});
聊天气泡文字自适应宽度
气泡左右留 20px padding,上下 12px,圆角 16px。先测文字宽,再画路径:
function drawBubble(ctx, text, x, y) {
const padding = 20;
const radius = 16;
const m = ctx.measureText(text);
const w = m.width + padding * 2;
const h = 36; // 固定高度,懒得算行高
roundRect(ctx, x, y, w, h, radius);
ctx.fillStyle = '#95EC69';
ctx.fill();
ctx.fillStyle = '#000';
ctx.fillText(text, x + padding, y + 24);
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
数据可视化里标签防重叠
柱状图顶部标签,如果两根柱子太近,文字会叠在一起。先测宽度,再判断是否会相交:
const labels = [
{ text: '1.2w', x: 120 },
{ text: '1.3w', x: 140 }
];
const margin = 8;
for (let i = 1; i < labels.length; i++) {
const prev = labels[i - 1];
const curr = labels[i];
const prevWidth = ctx.measureText(prev.text).width;
const currWidth = ctx.measureText(curr.text).width;
const prevRight = prev.x + prevWidth / 2;
const currLeft = curr.x - currWidth / 2;
if (prevRight + margin > currLeft) {
// 会重叠,把 curr 标签往右挤
curr.x += (prevRight + margin - currLeft);
}
}
输入框字数实时进度条(带 emoji 特殊处理)
emoji 在 JS 里长度是 2,但渲染宽度不一定等于两个英文字母。用 measureText 才能反映真实像素:
input.addEventListener('input', e => {
const val = e.target.value;
const m = offscreenCtx.measureText(val);
const ratio = Math.min(m.width / maxWidth, 1);
progressBar.style.transform = `scaleX(${ratio})`;
});
测不准?先查这三件事
- 字体真的加载完了吗?
别忘了document.fonts.ready,否则永远是 fallback 宽度。 - Canvas 上下文的
font属性写全没?
大小、字重、族名必须和fillText时完全一致,差一个空格都可能翻车。 - 测的是中文还是 emoji?
某些组合 emoji 宽度返回 0 不是 bug,是字体里根本没这个字形,浏览器用图片替代,测量拿不到尺寸。真要高精度,可用span.getBoundingClientRect()兜底:
function measureTextDomFallback(text, font) {
const span = document.createElement('span');
span.style.font = font;
span.style.whiteSpace = 'pre';
span.textContent = text;
document.body.appendChild(span);
const rect = span.getBoundingClientRect();
document.body.removeChild(span);
return { width: rect.width, height: rect.height };
}
骚操作开发技巧
- 缓存测量结果,别重复造轮子,前面代码已给模板。
- 用离屏 Canvas 预测文字尺寸,主屏丝滑如德芙:
const offscreen = new OffscreenCanvas(1, 1);
const offCtx = offscreen.getContext('2d');
// 所有测量走 offCtx,不影响主画布
- 遇到极端混合语言(中日韩 + emoji + 特殊符号),
measureText也头疼,可以「分段测量」:正则把 emoji、英文、中文切开,各自测宽再求和,误差降到 1px 以内。
最后唠叨一句
measureText 不是万能的,但不用它,你的 Canvas 文字永远在「差不多」边缘疯狂试探。今天把压箱底的笔记都掏出来了,谁再被设计师吐槽文字对不齐,就把这篇文章甩给他:「兄弟,尺子给你,自己量!」

140



被折叠的 条评论
为什么被折叠?



