前端开发里最容易被低估的一个问题,就是“字符宽度”。width: 200px明明写好了,用户输入第三个汉字就被截断;表格里中文和英文混排,列宽怎么调都对不齐;后端用substring(0, 10)截标题,列表出现“半截字”,只剩一个箭头或者半个省略号。
这些现象的根源其实很统一:你把“字符宽度”理解成了字符个数、固定像素或者等宽网格,实际上它是由字体度量、排版语言和上下文共同决定的动态值。如果你只在 CSS 里改宽度、在代码里数长度,永远是在猜,而不是在控制。
这篇文章想把“字符宽度”讲透。我会先说明字符宽度的底层概念,再分类讲 CSS 定宽、JS 动态计算宽度、后端按显示宽度截断这三条实践路径,最后给一份可直接使用的排错清单和工程建议。无论你是写前端页面、做表格组件,还是处理文本截断的后端接口,读完都能找到对应的方案。
1. 这篇文章真正要解决的问题
先定义一下讨论范围。本文说的“字符宽度”,不是指letter-spacing那种字距微调,也不是指word-spacing,而是指一个字符在最终渲染结果中实际占据的横向空间。它干扰开发者的形式有三种。
第一种,CSS 定宽失效。你给输入框设置了width: 200px,但输入一个汉字和输入 5 个英文字母,占用的视觉宽度完全不同。200px 能装下 13 个英文小写字母,却只能装下 6 个汉字。这说明px这个单位并不天然等于“显示多少个字符”。
第二种,中英文混排无法对齐。表格里一列放“姓名”,一列放“Name”,列宽始终无法统一。因为汉字大多是全角字符,英文字母是半角字符,全角和半角的横向占位比例在不同字体下差异很大。
第三种,后端截断把文本切坏了。业务需求经常是“标题最多显示 10 个字符”,开发直接substring(0, 10)。但 Java 的length()、Python 的len()统计的是 Unicode 码点数量,不是“看起来多宽”。一个 emoji、一个中文标点、一个全角括号,在显示宽度上和英文字母完全不等价。
有一个概念需要一开始就纠正:字符宽度 = 字符个体宽度 × 字体比例,不只看字符个数。谁在控制这个“字体比例”?是字体度量。看完下一节,你就知道为什么同样一段文字,在 Arial 和 Consolas 里占的宽度不一样。
1.1 哪些人最应该读这篇文章
如果你正在做以下事情中的任何一件,这篇文章是为你写的:
- 前端开发:做表单、表格、搜索框、列表,需要让元素宽度和字符数量对齐。
- 组件库作者:封装
Input、Tag、Table等组件,需要处理溢出、省略号、自适应宽度。 - 后端开发:写接口时做文本截断、列表标题截断、导出 Excel 时控制列宽。
- 运维或桌面端开发者:写命令行工具、终端表格时需要对齐输出,这本质也是字符宽度问题。
1.2 本文的核心判断
我的核心判断是:处理字符宽度,优先级应该是“测量优先、单位次之、截断兜底”。
能用ch单位表达“几个字符”的场景,就用ch;需要精确控制动态宽度时,用canvas.measureText或等价能力做测量;后端截断时,不能只按字符数切,要做显示宽度估算,并且给超长文本留回退。这样你不再需要对着像素猜宽度,而是真正知道一段文本在某个字体下到底有多宽。
2. 字符宽度基础:先搞懂字体度量
“字符宽度”这个概念不是 CSS 发明的,它来自字体排版。你页面上的每个字符,在字体文件里都有一组度量数据。
2.1 字符宽度不是“字符个数”
字体中,每个字符都有一个横向行进宽度(advance width),表示这个字符渲染后,绘制下一个字符时光标要前进多少距离。这是字符宽度的本质。
大写 W 在常见比例字体里宽度接近 1em;小写 i 只有 W 的 1/3;汉字“中”在浏览器默认字体中通常占 1em;半角逗号则可能只占 0.2em。所以“10 个字符”这个说法本身就有问题,除非你明确说“10 个等宽字符”。
2.2 等宽字体与比例字体:同一个 width 含义完全不同
字体按字符宽度是否一致,分为等宽字体和比例字体。
等宽字体每个字符占用的行进宽度相同,例如 Consolas、Courier New、中文场景下的部分 CJK 字体。等宽字体适合代码、命令行、表格对齐。
比例字体不同字符宽度不同,例如 Arial、Helvetica、微软雅黑。绝大多数系统默认 UI 字体是比例字体。
这就是为什么同样设置width: 200px,代码编辑器里能对齐,页面表格里对不齐——它们使用的字体类型不同。
2.3 全角与半角:中文排版的特殊问题
在中文排版语境下,还有一组概念:全角字符和半角字符。
全角字符通常占 1em,半角字符约占 0.5em 或更少。韩文、日文假名、中文标点、全角括号都属于全角;英文大小写、数字、半角标点属于半角。
但是要注意一个细节:“全角占 1em、半角占 0.5em”只是一个近似值。不同字体、不同字号下,半角英文字母的实际宽度并不严格等于 0.5em。在微软雅黑下,半角数字约占 0.55em;在 Arial 下,又不一样。所以严谨地说,全角/半角是排版方向上的近似分类,不是物理学上的精确规律。
| 概念 | 含义 | 举例 | 是否固定宽度 |
|---|---|---|---|
| 等宽字体 | 所有字符行军宽度一致 | Consolas、Courier New | 是全字体等宽 |
| 比例字体 | 字符宽度随字形变化 | Arial、微软雅黑 | 否 |
| 全角字符 | 宽度接近当前字号 | 中文、中文标点 | 约 1em |
| 半角字符 | 宽度约为字号一半或更少 | 英文、数字 | 约 0.5em |
3. CSS 中设置字符宽度的几种方式
CSS 里设置宽度,可选单位很多:px、em、rem、ch、ex。真正和“字符宽度”强相关的是ch和em。
3.1 px 与 width:固定像素不等于字符宽度
width: 200px是固定物理宽度,和字体没有任何关系。它不会因为你换了字体就自动变成“能容纳 N 个字符”。
如果你需要输入框能容纳固定数量的字符,不要把width写死成像素,否则在不同字体、不同字号下,实际容纳量完全不同。应该用ch,或在 JS 中动态测量后设置像素值。
3.2 em、rem、ch、ex 的区别与适用场景
先看这四个单位的对比:
| 单位 | 相对对象 | 典型用途 |
|---|---|---|
| em | 当前元素的 font-size | 设置内边距、缩进、按钮尺寸 |
| rem | 根元素的 font-size | 全局一致的缩放尺寸 |
| ch | 当前字体中数字 0 的宽度 | 按字符数量定宽 |
| ex | 当前字体的小写 x 高度 | 特殊排版场景 |
ch单位的设计初衷就是“一个字符的宽度”。在等宽字体下,1ch就是字体的行进宽度,width: 20ch差不多等于 20 个字符的宽度。但在比例字体中,ch只代表数字 0 的宽度,不代表每个字符都一样宽,所以它只能近似控制。
em适合做块级元素的整体尺寸。比如按钮内边距用0.5em 1em,会随字号变化,这是设计系统里的常见做法,但它不是“字符宽度”的控制手段。
3.3 用 ch 实现“按字符数量定宽”
如果你希望表单输入框“最多显示 8 个字符”,并且使用的是等宽字体,ch是最直接的工具。
/* 文件路径:style.css */ .input-code { width: 10ch; font-family: Consolas, "Courier New", monospace; box-sizing: border-box; padding: 0.25em 0.5em; }对应 HTML:
<!-- 文件路径:index.html --> <label> 验证码 : <input class="input-code" maxlength="10" placeholder="请输入验证码"> </label>这样设置后,输入框宽度基础就是 10 个字符,而不是 120px 等固定值。要注意box-sizing: border-box会让 width 包含 padding 和 border,实际内容区可能小于 10ch,所以如果严格要“正好容纳 10 个字符”,最好先把 padding 算清楚。
3.4 文本溢出与换行对宽度的影响
字符宽度的另一个常见场景是“文本放不下”。CSS 中处理这个问题的核心属性是overflow-wrap、word-break和text-overflow。
/* 文件路径:style.css */ .ellipsis { max-width: 20ch; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }text-overflow: ellipsis是控制显示省略号的关键,但它只能在块级容器且overflow: hidden; white-space: nowrap;同时存在时生效。
这里真正容易踩坑的地方是:max-width的单位。如果你用20ch,在等宽字体下是 20 个字符,但在比例字体下,小写 i 很窄、大写 W 很宽,最终显示效果完全不是 20 个普通字符。所以“用 ch 做 ellipsis 容器宽度”只适合等宽字体场景;否则,你要根据实际内容动态测量。
4. 中英文混排中的字符宽度陷阱
中英文混排是把“字符宽度”问题放大得最明显的场景。中文是方块字,宽度近似一致;英文是比例字,宽度差异很大。二者混排时,如果直接按固定宽度处理,就会出现各类对齐问题。
4.1 为什么 input 无法精确显示 N 个字符
假如你希望输入框能容纳“3 个中文 + 5 个数字”,设置width: 8ch会怎样?在默认 UI 字体下,ch基于数字 0 的宽度,但汉字“中”通常比数字 0 宽,所以真实结果可能是只装下 6 个字符就溢出。
如果你的需求是“固定容纳 N 个汉字”,更稳妥的方式是用em。因为汉字宽度接近 1em,width: 8em大约可以容纳 8 个汉字。但如果你要混排中文和英文,em也会失效,因为英文不是 1em。
所以结论是:复杂文本不要依赖单一 CSS 单位去模拟字符宽度,动态测量是更可靠的方式。
4.2 表格纵向对齐的常见错误
表格中对齐有两种常见诉求:数字列右对齐、文本列按宽度截断。
数字列右对齐时,不要用固定width,应该让列宽取决于数字的最大位数,同时用font-variant-numeric: tabular-nums让数字变成等宽数字。否则,同样是 4 位数字,“1111”和“8888”宽度不同,会导致数字列视觉上参差不齐。
/* 文件路径:style.css */ .num-cell { font-variant-numeric: tabular-nums; text-align: right; }tabular-nums让数字使用等宽数字字形,这是处理数字列对齐的最快方案。
文本列截断时,则要回到 3.4 节的 ellipsis 方案。表格列宽建议用比例(百分数)或用min-width+max-width结合,不要用固定ch,因为单元格内容中英文混合,ch无法表达真实宽度。
4.3 textarea、input 与中文数字混排的实际场景
如果是一个“商品编码 + 商品名称 + 数量”的列表,编码段是全角还是半角、数字是几位精度,都会直接影响列宽。实际工程里更推荐这样处理:
- 数字、英文、中文分别设定不同的宽度标准。
- 超长内容优先使用省略号而不是换行。
- 需要精确对齐时,后端返回展示宽度或前端动态测量。
下面一节讲的就是动态测量如何落地。
5. 动态计算字符宽度:canvas.measureText 实践
CSS 单位适合静态、近似场景。当你要做“根据内容自动拉伸输入框宽度”“让标签文本宽度等于内容宽度”“省略号按真实像素截断”这类动态需求时,必须测量文本的渲染宽度。
浏览器为这个需求提供了一个底层 API:canvas.measureText()。
5.1 为什么需要动态测量
假设有个“标签”组件,内容可以是中文、英文、数字混合,你需要让标签的背景宽度刚好容纳文字,并在超过容器宽度时显示省略号。用百分比或 ch 都无法准确计算,因为单词间的空隙、字符的宽度、字体的粗细都会影响结果。此时只能用真实渲染引擎计算。
canvas.measureText返回文本在当前字体配置下的像素宽度,这与浏览器实际渲染结果非常接近。
5.2 JavaScript 实现文本宽度测量
// 文件路径:textWidth.js function getTextWidth(text, font) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.font = font || getComputedStyle(document.body).font; const metrics = ctx.measureText(text); return metrics.width; } // 用法 const w = getTextWidth('测试中文ABC123', '14px "Microsoft YaHei", sans-serif'); console.log(w); // 单位是 CSS 像素这段代码的要点:先创建一个 canvas 上下文,设置字体,然后调用measureText。要注意两点:
- 必须保证传入的
font字符串与页面实际使用的字体一致,否则测出来是错的。 measureText返回的对象还有actualBoundingBoxLeft、actualBoundingBoxRight等字段,它们表示字符实际着色的边界,比width更精确,但width已经能满足绝大多数布局需求。
5.3 动态控制输入框宽度案例
一个典型需求是:输入框宽度随输入内容变化。比如邮件标签填写时,输入几个字符,框就跟着变宽,但不超过最大宽度。
// 文件路径:autoWidthInput.js function autoResizeInput(input, maxWidth) { const font = getComputedStyle(input).font; const width = getTextWidth(input.value || input.placeholder, font); const target = Math.min(Math.max(width + 20, 40), maxWidth); input.style.width = target + 'px'; } document.querySelectorAll('.auto-width-input').forEach((input) => { input.addEventListener('input', () => autoResizeInput(input, 320)); });常见问题是:输入框的边框和 padding 会额外占宽度,所以要预留余量(示例中+ 20就是给 padding 和 border 的补偿)。如果出现抖动,可以改用requestAnimationFrame节流:
let rafId; input.addEventListener('input', () => { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => autoResizeInput(input, 320)); });如果运行后输入框依然出现内容溢出,第一步要检查getComputedStyle(input).font返回的字体是否与页面渲染字体一致。尤其是input默认字体和body字体不一致的情况很常见,这会直接导致测量偏差。
6. 表单与表格对齐中的字符宽度实战
表单和表格是前端开发里字符宽度问题的高发区,这里集中给出可直接套用的方案。
6.1 表单 label 对齐最佳方式
表单 label 需要左对齐、右对齐或者顶部对齐时,很多人直接设置固定 width。
更好的做法是:让 label 宽度能容纳最长文案,并加上一两个汉字的余量。由于中文几乎等宽,可以用em单位。
/* 文件路径:style.css */ .form-item { display: flex; align-items: center; margin-bottom: 12px; } .form-item .label { flex: 0 0 auto; min-width: 6em; /* 支持 4~5 个汉字左右 */ text-align: right; padding-right: 0.5em; }这里min-width: 6em而不是width: 6em,关键在长 label 时可以自动扩展,短 label 时保持统一右对齐。这对“字符宽度”的应用是一个重要经验:宽度控制尽量用 min-width/max-width,而不要锁死 width,让内容有自然伸缩的空间。
6.2 表格列宽设计的字符宽度思路
表格列宽建议按内容类型分类:
| 内容类型 | 宽度策略 | 说明 |
|---|---|---|
| 数字列 | table-layout: fixed+tabular-nums | 数字宽度稳定,容易对齐 |
| ID/编码 | 等宽字体 +ch或固定百分比 | 编码类似等宽文本 |
| 中文文本 | min-width+ ellipsis | 不要写死,要允许伸缩 |
| 超长描述 | 剩余宽度 + ellipsis | 给最后一列分配剩余空间 |
这里真正容易踩坑的是:table-layout: fixed会让列宽完全由表格第一行决定,超出部分要么溢出要么裁剪;table-layout: auto又让浏览器根据内容自动计算宽度,可能会发生列宽跳动。如果你的表格要稳定对齐,建议展示层用 fixed,但列宽值用百分比或者 min-width 控制。
/* 文件路径:style.css */ .grid-table { table-layout: fixed; width: 100%; border-collapse: collapse; } .grid-table .col-id { width: 10ch; font-family: Consolas, monospace; } .grid-table .col-name { min-width: 8em; } .grid-table .col-desc { width: auto; }col-desc的width: auto会占据剩余宽度,配合单元格内部 ellipsis,既能自适应,又不会把表格撑乱。
7. 后端字符串截断与显示宽度计算
前后端分离后,很多字符串截断逻辑由后端完成。但后端没有渲染引擎,无法直接测量像素宽度,所以最常见的错误就是直接用字符串长度截断。
7.1 String.length() 为什么不能直接用
Java 的String.length()按 UTF-16 码元计数。一个中文汉字是一个码元,一个 emoji 可能是两个码元,一个全角括号是一个码元,一个英文字母也是一个码元。按它截断,会出现两个问题:
- 把 emoji 从中间截断,产生乱码。
- 截断结果在显示上参差不齐:同样是 10 个码元,“十个中文汉字”和“十个英文字母”在屏幕上占用的宽度差一倍。
7.2 Java 按显示宽度取子串
后端处理中文文本截断,业务侧通常接受“半角算 1、全角算 2”的近似宽度。下面提供一个常用的 Java 实现,核心是遍历字符,累计显示宽度,到阈值后停止并输出省略号。
// 文件路径:TextWidthUtil.java public class TextWidthUtil { public static int displayWidth(String text) { if (text == null) { return 0; } int width = 0; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c >= '\u1100' && (c <= '\u115F' || c == '\u2329' || c == '\u232A' || (c >= '\u2E80' && c <= '\uA4CF') || (c >= '\uAC00' && c <= '\uD7A3') || (c >= '\uF900' && c <= '\uFAFF') || (c >= '\uFE10' && c <= '\uFE19') || (c >= '\uFE30' && c <= '\uFE6F') || (c >= '\uFF00' && c <= '\uFF60') || (c >= '\uFFE0' && c <= '\uFFE6'))) { width += 2; } else { width += 1; } } return width; } public static String truncateByWidth(String text, int maxWidth) { if (text == null) { return ""; } int width = 0; StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); int charWidth = displayWidth(String.valueOf(c)); if (width + charWidth > maxWidth) { sb.append("…"); break; } sb.append(c); width += charWidth; } return sb.toString(); } }上面的代码用了String.valueOf(c)做了简单判断,日常够用。如果你要处理代理对(例如 emoji),就必须用codePointAt并按 codePoint 遍历,否则会把 emoji 拆开。更稳妥的方式是引入通用库或自己按 codePoint 处理。
// 文件路径:TextWidthUtilWithEmoji.java public static String truncateByDisplayWidthSafe(String text, int maxWidth) { if (text == null) { return ""; } int width = 0; StringBuilder sb = new StringBuilder(); int index = 0; while (index < text.length()) { int codePoint = text.codePointAt(index); int charWidth = isWideCodePoint(codePoint) ? 2 : 1; if (width + charWidth > maxWidth) { sb.append("…"); break; } sb.appendCodePoint(codePoint); width += charWidth; index += Character.charCount(codePoint); } return sb.toString(); } private static boolean isWideCodePoint(int codePoint) { return displayWidth(new String(Character.toChars(codePoint))) == 2; }注意:这个实现同样基于近似规则,会占用较多字符。对于精确控制生产环境,建议用现成库,例如 ICU 的UCharacter.getIntPropertyValue或专门的 EastAsianWidth 属性判断。
7.3 Python 按显示宽度计算
Python 中同样可以按半角 1、全角 2 做计算。要处理 emoji 组合序列,建议用unicodedata和字符宽度属性做近似判断。
# 文件路径:text_width.py import unicodedata def display_width(char: str) -> int: # 将控制字符和组合字符视为 0 if unicodedata.combining(char): return 0 # East Asian Width 为 Wide 或 Fullwidth 的字符按 2 计算 eaw = unicodedata.east_asian_width(char) if eaw in ('W', 'F'): return 2 return 1 def truncate_by_width(text: str, max_width: int) -> str: width = 0 result = [] for char in text: w = display_width(char) if width + w > max_width: result.append('…') break result.append(char) width += w return ''.join(result) if __name__ == '__main__': s = "测试中文字符ABC" print(display_width(s[0])) # 2 print(truncate_by_width(s, 8)) # 例子这里unicodedata.east_asian_width是判断中文字符宽度的标准方法。实际生产环境中如果只处理普通文本,这套逻辑足够;如果包含 emoji 组合序列,建议再引入unicodedata与正则进行 ZWJ 序列处理,避免把一个 emoji 拆成两半。
7.4 一个工程级别的安全建议
后端截断最容易犯的错误是:直接截断后拼上省略号,但忽略了字符边界、代理对和组合字符。建议在任何截断函数里都遵循以下顺序:
- 先按 codePoint 遍历,而不是按
char遍历。 - 遇到代理对、组合字符、ZWJ 序列时,把整个序列当作一个“显示单元”。
- 截断前记录原始字符串,如果业务不确定,宁可返回原串也不要产生乱码。
- 给截断函数写单测,覆盖:纯中文、纯英文、中文+emoji、
maxWidth正好等于边界值的情况。
8. 常见问题与排查思路
字符宽度相关的报错通常不是“抛异常”,而是“显示不符合预期”,所以排查时需要有明确的检查顺序。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 设置了 width: 200px,输入框还是放不下中文 | px 和字符宽度没有换算关系 | 用 canvas.measureText 测量实际字符宽度 | 改用 ch/em 或动态测量 |
| 用 ch 设置宽度后,英文小写 i 和 W 对不齐 | 比例字体下 ch 只基于数字 0,不代表每个字符 | 检查字体是否为等宽字体 | 切换到等宽字体或动态测量 |
| 表格数字列看起来忽宽忽窄 | 数字在比例字体下宽度不一致 | 检查是否设置tabular-nums | 添加font-variant-numeric: tabular-nums |
| text-overflow: ellipsis 不生效 | 缺少 overflow: hidden 或 white-space: nowrap | 检查三者是否同时设置 | 补全样式 |
| 后端截断出现“半个 emoji” | 截断按 UTF-16 char 遍历 | 检查日志输出是否出现孤立代理项 | 按 codePoint 遍历 |
| Python 截断中英文结果不整齐 | len() 按 Unicode 码点计数,不是显示宽度 | 用east_asian_width检查 | 改用 display_width 函数 |
| 动态测量宽度和实际渲染有偏差 | 字体设置不一致 | 检查 getComputedStyle().font | 统一字体字符串 |
9. 最佳实践与工程建议
前面把概念和实践都讲完了,最后把经验沉淀成一组可以直接执行到团队规范里的建议。
9.1 前端定宽的优先级
在实际代码里,我建议按以下优先级选择字符宽度方案:
- 能用
em表达中文场景(例如 label 宽度、按钮宽度),优先用em。 - 能用
ch表达等宽字体场景(验证码、编码输入),优先用ch。 - 需要精确控制时,用
canvas.measureText动态计算。 - 尽量不要直接用固定
px去模拟字符数量,除非你确认字体不会再改。
9.2 动态测量时的字体统一
使用canvas.measureText时,最容易出问题的不是测量本身,而是字体不一致。页面里input的字体和body的字体可能不同;CSS 变量在某些浏览器里解析方式不同。建议封装一个全局文本测量函数,统一读取getComputedStyle,避免在组件里散落字体字符串。
// 文件路径:textWidth.js const textMeasurer = { canvas: document.createElement('canvas'), ctx: null, init() { if (!this.ctx) { this.ctx = this.canvas.getContext('2d'); } }, measure(text, element) { this.init(); const font = element ? getComputedStyle(element).font : getComputedStyle(document.body).font; this.ctx.font = font; return this.ctx.measureText(text).width; } };后续组件直接调用textMeasurer.measure('测试', inputEl),避免重复创建 canvas。
9.3 后端截断的安全边界
后端截断时,务必要考虑“显示宽度”而不是“字符数量”。如果团队没有统一的截断工具类,建议单独抽一个模块并遵循:
- 输入为空、maxWidth <= 0 时直接返回原串或空串。
- 截断结果必须拼接省略号时,省略号本身的宽度也要计入 maxWidth。
- 对半角/全角宽度采用可配置策略,默认半角 1、全角 2。
- 对代理对、组合字符、emoji 要额外处理。
- 写单元测试,防止后续改动破坏边界行为。
9.4 验收清单
一个字符宽度功能开发完成后,可以按这张清单交付:
- 固定宽度元素在中文、英文、数字三种输入下的表现符合预期。
- 超长文本能看到省略号,而不是溢出容器。
- 表格列宽在不同数据量下不会抖动。
- 后端截断不会出现乱码、半个 emoji、半个代理对。
- 动态测量逻辑在浏览器缩放下表现稳定(考虑
devicePixelRatio和缩放,测量通常不需要处理 DPR,但要注意 CSS 像素与设备像素是两个概念)。 - 涉及生产环境修改时,先在小范围灰度,再全量发布。
10. 总结与后续学习方向
字符宽度不是单一属性,而是字体、单位、渲染上下文共同作用的结果。前端定宽时,优先用语义单位;精确计算时,用 canvas 测量;后端处理时,按显示宽度截断。这三条线并不是彼此孤立的,一个完整系统通常都需要同时覆盖。
如果你还想继续深入,可以从这几个方向拓展:一是字体度量中em square、ascent、descent对行高的影响,这直接关系到多行文本的垂直对齐;二是 CSStext-rendering和字体渲染对宽度的影响,在低端设备上字符间距可能有微小差异;三是 ICU 的 EastAsianWidth 属性,后端做国际化文本处理时建议直接用它作为宽度标准。
建议收藏本文,实际写组件或截断工具时拿回来对照。字符宽度这件事,并不需要靠经验拍脑袋,测量、单位、兜底,三个环节做好,你的页面和接口就不会再因为“宽度”翻车。