4. 字体与文本渲染:字体回退机制、中文字体兼容性、行高与基线对齐问题、富文本解析差异

字体移植,看着简单,其实坑最多。

我见过太多团队,UI 稿上漂漂亮亮,一跑到目标设备上,文字要么挤成一团,要么上下错位,要么直接变成方框。说白了,字体渲染这件事,每个平台都有自己的小脾气。

这一章,我把这些年踩过的坑,一个一个说清楚。

4.1 字体回退机制:别让用户看到「豆腐块」

什么叫「豆腐块」?就是本该显示文字的地方,出现了一个个方框或者问号。用户看到这个,第一反应就是「这 App 坏了」。

为什么会这样?因为目标设备上没有你指定的字体。

核心原则:永远不要只写一个 font-family。你要给系统留足「回退」的余地。

我个人习惯的写法是这样的:

/* 推荐的回退策略 */
.font-safe {
  font-family: 
    "PingFang SC",      /* iOS 中文首选 */
    "Microsoft YaHei",  /* Windows 中文首选 */
    "Noto Sans CJK SC", /* 跨平台开源方案 */
    "Source Han Sans SC",/* 思源黑体,备选 */
    sans-serif;         /* 最后兜底 */
}

.font-serif {
  font-family: 
    "Noto Serif CJK SC",
    "Source Han Serif SC",
    "SimSun",
    serif;
}

这里有个细节要注意:字体名称的引号不能省。尤其是中文字体名中间有空格时,不加引号会被浏览器解析错误。

小技巧:我在项目中习惯把字体回退链写成「平台首选 → 跨平台开源 → 系统兜底」三层结构。这样不管用户用什么设备,至少能保证「能看」。

4.2 中文字体兼容性:你以为的「一样」其实不一样

中文字体移植,最头疼的是什么?

是同一个字体名,在不同平台上的渲染效果完全不同。

举个例子:"PingFang SC" 在 macOS/iOS 上表现完美,但到了 Windows 上,系统会直接跳过它,去找下一个回退字体。如果你没写回退,那就等着出「豆腐块」吧。

我整理了一份常用中文字体的跨平台兼容表:

字体名称 macOS / iOS Windows Android Linux
PingFang SC ✅ 原生 ❌ 无 ❌ 无 ❌ 无
Microsoft YaHei ❌ 无 ✅ 原生 ❌ 无 ❌ 无
Noto Sans CJK SC ✅ 可安装 ✅ 可安装 ✅ 部分预装 ✅ 常见
Source Han Sans SC ✅ 可安装 ✅ 可安装 ⚠️ 需配置 ✅ 常见
SimSun (宋体) ⚠️ 兼容模式 ✅ 原生 ❌ 无 ❌ 无

避坑指南:我曾经在做一个跨平台 SDK 时,直接用了 "PingFang SC" 作为唯一字体。结果 Windows 测试机上所有中文都变成了默认宋体,行高全乱。从那以后,我每条 font-family 都至少写三个回退。

4.3 行高与基线对齐问题:为什么文字总是「上下不居中」?

这个问题,UI 设计师最敏感。

设计稿上文字完美居中,到了代码里,要么偏上,要么偏下。你调了半天 line-height,还是不对。

嗯,这里要注意:不同平台对「基线」的定义不一样。

我画了一张图,帮你理解这个差异:

不同平台文字基线对齐对比 iOS (PingFang SC) 中文基线测试 基线偏上 行高 1.5 倍 Android (Noto Sans) 中文基线测试 基线居中 行高 1.5 倍 Windows (微软雅黑) 中文基线测试 基线偏下 行高 1.5 倍 同一行高值,不同平台渲染高度差异可达 2-4px

你看,同样的字号、同样的行高值,在不同平台上的实际渲染高度是不一样的。这就是为什么你调好了 iOS,Android 又歪了。

我的解决方案是:

  • 不要用固定的 line-height 值。用相对值,比如 line-height: 1.5,让平台自己算。
  • 针对每个平台微调。我习惯写一个 mixin 或者工具函数,按平台做 ±1px 的补偿。
  • 用 flex 布局辅助居中。对于单行文字,用 display: flex; align-items: center; 比纯 line-height 更可控。

实战代码:

/* 跨平台行高补偿方案 */
.text-block {
  font-size: 16px;
  line-height: 1.6;  /* 相对值,平台自适应 */
  
  /* iOS 补偿 */
  @supports (-webkit-touch-callout: none) {
    padding-top: 1px;
  }
  
  /* Android 补偿 */
  @supports (-webkit-appearance: none) and (not (-webkit-touch-callout: none)) {
    padding-bottom: 1px;
  }
}

4.4 富文本解析差异:同一个 HTML,不同的「长相」

富文本移植,是字体渲染里最隐蔽的坑。

你想想看,同样的 <strong> 标签,iOS 的 UILabel 解析出来是系统粗体,Android 的 TextView 可能直接忽略,WebView 又有一套自己的渲染规则。

我遇到过最典型的问题:

  • 加粗效果不一致:有些平台对 font-weight: 600 和 700 的渲染完全一样,有些则区分明显。
  • 斜体支持缺失:中文字体很多没有斜体字重,平台会强行「伪斜体」,效果惨不忍睹。
  • 列表缩进差异:<ul> 和 <ol> 的默认 padding 各平台不同,导致排版错位。

我建议的做法是:

  1. 富文本内容尽量用最基础的标签:<b>、<i>、<u> 比 <strong>、<em> 更稳定。
  2. 不要依赖默认样式:每个平台都显式重置一遍 margin、padding、font-weight。
  3. 用属性替代标签:比如用 color 属性代替 <font> 标签。

避坑指南:我曾经在移植一个新闻阅读器时,后台返回的富文本里用了 <span style="font-weight: bold;">。iOS 原生渲染没问题,但 Android 的 TextView 直接忽略了行内样式。最后我不得不在客户端做了一层「样式转属性」的预处理。

4.5 本章小结

字体与文本渲染,说白了就是「别替用户做决定」。你不知道用户设备上装了什么字体,也不知道平台会怎么渲染你的文本。

你唯一能做的,就是:

  • 写好回退链,保证「有字可显」
  • 做好平台适配,保证「位置不偏」
  • 简化富文本,保证「样式不乱」

做到这三点,字体移植的坑,你至少能避开八成。