SimHei 这个字体名,凡是做过中文报表、扫描件还原、老系统界面复刻、批量 PDF 生成的人,基本都绕不开它。网上搜“SimHei 字体 永久有效”的人,诉求通常很朴素:想要一个装上去就不会哪天突然失效、换台机器也不会变成方块的黑体。但字体这东西的“永久有效”,从来不是文件本身说了算,而是由三层东西共同决定的——本机有没有装、目标环境认不认、授权允不允许你这么用。我这篇就把这三层拆开讲,从字体文件的技术结构、授权边界,一路讲到 Linux 服务器和容器里的部署、字体子集化、代码侧指定字体、以及渲染出问题时的排查方法。适合后端、数据可视化、前端、以及做文档自动化的人看,也适合只是想在本地把字体装对、不想再被方块字折磨的朋友。
1. 把“永久有效”翻译成工程语言:SimHei 的真实身份与边界
1.1 一个字体文件里到底装了什么
很多人对字体的认知停留在“一个 .ttf 文件,双击安装就完事”。但只要你在服务器上被方块字坑过一次,就会明白字体文件其实是一份结构相当复杂的二进制数据,里面至少有几张表在决定它能不能被正确识别和使用。
最关键的是name表,它记录了字体家族名(family)、子家族名(subfamily)、PostScript 名、唯一标识等。操作系统和渲染引擎认字体,第一件事就是读这张表。SimHei 的家族名就是SimHei,子家族是Regular,PostScript 名同样是SimHei。你在 CSS 里写font-family: SimHei、在 matplotlib 里写font.sans-serif = ['SimHei'],底层匹配的就是这个名字,而不是文件名。这一点极其重要——把文件重命名成hei.ttf不影响家族名匹配,但如果你拿到的是一份被改过name表的“精简版”,那所有按名字匹配的代码都会失效。
其次是字形轮廓表。SimHei 走的是 TrueType 轮廓路线(glyf+loca表),而不是 CFF/OpenType 那套三次贝塞尔。这带来的直接影响是:它在低分辨率屏幕上靠 TrueType 的网格拟合指令(hinting)能撑住小字号的可读性,但在高分辨率打印和大幅面输出时,笔画末端的处理不如现代多字重字体细腻。另外,它只有一个字重,也就是 Regular,没有 Bold、Light 这些变体。你在 Word 里点“加粗”看到的效果,绝大多数情况下是渲染器用算法合成的伪粗体(faux bold),把笔画横向“膨胀”一下,字怀会被糊住,小字号下尤其难看。
还有字符覆盖范围。SimHei 主要覆盖 GB2312 和 GBK 区间的汉字,常规的 6763 个简体字加上扩展部分基本够用,日常办公、公文、报表完全没问题。但如果你要处理人名里的生僻字、古籍用字、或者 GB18030 四字节区的内容,它就会露怯——不是显示成方块,就是被 fallback 到别的字体,导致同一行字粗细、风格不一致,这在正式的文档输出里是很难看的。
最后是文件体积。完整的 SimHei 大概在 9 到 10 MB 这个量级,因为它把两万多个字形全部打包进去了。放到网页上直接加载是不现实的,塞进移动端 App 包里也要掂量掂量,这就引出了后面要讲的子集化。
注意:判断一份字体文件是不是完好,不要只看能不能双击预览。用
fc-query(Linux/macOS)或字体查看工具读一下name表和字形数量,才是靠谱的验证方式。
1.2 “永久有效”的三种常见误解
第一层误解在授权上。SimHei 是随部分桌面操作系统分发的系统字体,你在自己的机器上用它是没问题的,这是随系统授权的一部分。但“我在自己电脑上能永久用”和“我可以把它打包进产品里分发”完全是两码事。把字体文件抠出来放进 App 安装包、上传到自己的静态资源服务器做@font-face加载、嵌入到对外发布的电子书里,这些行为是否被允许,取决于你获取该字体的途径和相应的许可条款,而不是文件能不能复制。我见过不少团队在这个环节翻车:设计稿用了系统黑体,前端一看本地有就直接打进包里,上线半年后被要求整改,返工成本极高。
第二层误解在技术上。字体文件当然不会“过期”,不存在用了三年突然打不开的情况。真正会变的是运行环境。你在本地 Windows 上调试,一切正常;代码扔到 Linux 构建机上跑,中文全变方块。原因很简单——那台机器根本没装这个字体。所以“永久有效”在工程语境下的正确理解应该是:目标环境的字体可用性可控,而不是文件本身不会消失。
第三层误解在兼容上。老系统里预装的字体,不代表新系统、移动端、云端渲染服务里还有。尤其在容器化之后,一个几百 MB 的精简基础镜像里通常只有 DejaVu 这类拉丁字体,中文字体一个都没有。你的代码在本地跑得好好的,一进 CI 就产出一堆豆腐块,本质上就是这个原因。
把这三层想清楚,后面的所有操作才有意义:授权上守住边界,技术上保证目标环境有字体,兼容上用 fallback 兜底。
2. 选型决策:什么场景该留 SimHei,什么场景必须换
2.1 SimHei 的技术特征决定了它的舒适区
我先把话说在前面:SimHei 不是“过时字体”,它在特定场景下依然是合理选择,硬要替换反而增加成本。
它的第一个优势是字形辨识度高。黑体本身就是为屏幕和低质量印刷设计的,笔画粗细均匀、结构方正,在小字号、低 DPI、扫描件、传真件这类“信息衰减严重”的媒介上,比宋体、楷体更容易辨认。做票据识别、档案数字化、老系统截图还原的项目,用 SimHei 往往比用现代无衬线字体更贴近原始效果。
第二个优势是系统预装率高。在桌面办公环境里,它几乎是默认存在的,这意味着你生成的文档发给别人,对方打开时大概率不会跑版。这一点在跨单位流转的报表、通知类文档里价值很大——你没法要求接收方去装字体。
第三个优势是与既有排版规范的契合。很多公文、报表、内部文档的格式规范里,黑体就是标题层级的指定字体之一。这种情况下你换字体,等于改了规范,属于给自己找麻烦。
但它的短板同样明确,而且是结构性的:
- 单字重。做有层级的设计、需要靠字重区分信息主次时,它撑不起来。硬用伪粗体,观感会很廉价。
- 拉丁字符设计薄弱。英文和数字的字形是早期为了“补全字符集”而做的,字宽、字距与中文字形的协调性一般,纯英文排版很难看。
- 无 OpenType 高级特性。竖排标点、局部化替换、字形替代这些都没有,做复杂排版会受限。
- 字符集覆盖不到 GB18030 全量。生僻字场景直接出局。
- 授权边界不清。这是最需要谨慎的一条,尤其涉及再分发。
所以我的判断标准很朴素:内部流转、屏幕阅读、老系统还原、规范指定,用 SimHei;对外发布、品牌视觉、移动端内置、需要多字重层级、需要全字符集,换掉。
2.2 替代方案横向对比与迁移成本评估
真要替换,选哪个?我把常见的几类开源中文字体列出来做个对照。这些字体都可以自由使用甚至商用(具体以各自的许可文本为准,用之前务必读一遍),迁移成本主要看你现有的字体声明散落在多少个地方。
| 字体 | 字重数量 | 字符覆盖 | 轮廓类型 | 体积量级 | 适合场景 |
|---|---|---|---|---|---|
| SimHei | 1 | GBK 为主 | TrueType | 约 9-10 MB | 内部报表、老系统还原、规范指定 |
| 思源黑体 / Noto Sans CJK | 7 | 覆盖极广,含多语言 | CFF / TrueType 两版 | 单字重 8-16 MB | 通用替代首选,设计与前端 |
| 更纱黑体 | 多字重 | 广 | TrueType | 较大 | 需要中西文混排协调的代码/文档 |
| 文泉驿微米黑 | 1-2 | GBK 级 | TrueType | 约 4-5 MB | Linux 服务器兜底、体积敏感 |
| 霞鹜文楷 | 1-3 | 较广 | TrueType | 中等 | 需要楷体质感的正文排版 |
迁移时的成本主要不在字体文件本身,而在三处:
一是字体栈的声明点。CSS 里的font-family、绘图库里的rcParams、PDF 模板里的样式、Office 模板里的字体设置,这些东西往往散落在十几个文件里,没人统一管。我的做法是先全局搜一遍字体名,把所有出现位置列成清单,再决定哪些改、哪些保留。
二是度量差异导致的排版偏移。不同字体的字面高度、字宽、行距默认值都不一样,换完之后原本刚好一页的报表可能溢出到第二页。这个问题在自动生成 PDF 的场景里特别烦,必须留出回归验证的时间。
三是字重映射。原来只有 Regular 一个字重,所有“加粗”都是合成的;换成七字重字体后,原来那些伪粗体会变成真实字重,视觉上会明显变轻(因为真粗体不像算法膨胀那么夸张)。这时候要么统一调字号,要么在样式里显式指定更重的字重。
提示:如果只是想让代码在 Linux 上不出方块,不追求视觉统一,最省事的方案是装一套文泉驿微米黑做兜底,同时在 fontconfig 里把
SimHei别名映射过去。这样业务代码一行都不用改。
3. 一次完整的字体部署实操:从本地到服务器
3.1 本机安装与字体目录的规范做法
先讲本地。不同系统放字体的位置不一样,搞清楚这件事能省掉大量“明明装了却找不到”的麻烦。
Windows 上有两个位置:系统级在C:\Windows\Fonts,需要管理员权限;用户级在%LOCALAPPDATA%\Microsoft\Windows\Fonts,装完只对当前用户生效。后者在多用户机器上更稳妥,不会影响别人。
macOS 上有三个层级:~/Library/Fonts只对当前用户生效,/Library/Fonts对所有用户生效,/System/Library/Fonts是系统保留位置,不要往里塞东西。日常装第三方字体,放前两个就够。
Linux 上遵循 XDG 规范:用户级在~/.local/share/fonts,系统级在/usr/share/fonts,放在后者下面建议再按来源分子目录,比如/usr/share/fonts/custom/,方便日后清理。
文件权限要特别注意。字体文件一般设成644、目录设成755就够了。我遇到过一次线上事故,运维把字体目录整体设成了600,导致非 root 用户跑的服务读不到字体,报错信息还是一句“字体找不到”,排查了快半天。
装完之后,Linux 上必须刷新字体缓存:
# 刷新用户级缓存 fc-cache -fv ~/.local/share/fonts # 刷新系统级缓存 sudo fc-cache -fv # 验证是否被识别 fc-list | grep -i simhei # 查看某个中文字体的详细信息 fc-query /usr/share/fonts/custom/SimHei.ttffc-list :lang=zh这条命令我很推荐,它能直接列出系统里所有支持中文的字体。在一台新机器上先跑一遍,你就知道这台机器到底有没有中文渲染能力,比瞎猜快得多。
还有一点容易被忽略:字体缓存文件本身也会出问题。~/.cache/fontconfig里的缓存如果损坏或者版本不匹配,会出现“文件明明在,fc-list也能看到,但程序就是匹配不上”的诡异情况。解决办法简单粗暴——删掉缓存目录重新生成:
rm -rf ~/.cache/fontconfig fc-cache -fv3.2 Linux 服务器与容器环境的字体落地
服务器环境才是字体问题的重灾区。一台干净的 Linux 服务器,尤其是最小化安装的,中文字体一个都没有。你的 Python 脚本生成图表,中文标题全变方块;你的无头浏览器截图,页面中文全是豆腐块。这不是代码问题,是环境问题。
容器里更极端。基于 Alpine 的镜像通常只带 DejaVu 系列,连中文标点都不全。基于 slim 版 Debian 的镜像同样如此。我一般的处理顺序是:先看能不能直接装包,装不了再手工拷字体。
# Debian / Ubuntu 系 apt-get update && apt-get install -y fonts-wqy-zenhei fonts-wqy-microhei # Alpine 系 apk add --no-cache font-noto-cjk font-noto-cjk-extra # 刷新缓存 fc-cache -fv如果因为网络策略或镜像体积限制不能装包,那就把已经准备好的字体文件随项目一起放进镜像:
FROM python:3.11-slim # 复制字体并建立目录 COPY assets/fonts/ /usr/share/fonts/custom/ RUN chmod 644 /usr/share/fonts/custom/*.ttf \ && fc-cache -fv # 验证:找不到就构建失败,别等到运行时才发现 RUN fc-list :lang=zh | grep -q . || (echo "no CJK font found" && exit 1)最后那行验证是我强烈建议加的。用构建期的失败去替代运行时的方块字,能省掉很多事后排查。很多人镜像构建完就跑,等到线上生成了一份满是方块的报表才发现问题,那时候要重新走一遍发布流程。
在容器里还要注意 locale 设置。有些程序会根据 locale 决定用哪个字体族,LANG=C.UTF-8和LANG=en_US.UTF-8在字体回退链上的表现可能不同。稳妥的做法是显式指定LANG=C.UTF-8,并且在代码层面显式指定字体,不依赖系统自动匹配。
另外,如果你在容器里跑的是无头浏览器做截图或 PDF,光有字体文件还不够,浏览器的字体配置也要能读到。Chromium 系的容器镜像通常会读/etc/fonts/fonts.conf和~/.config/fontconfig/fonts.conf,把字体放在标准目录下最省心,别放在什么/app/res里指望它自己找到。
3.3 fontconfig 别名映射:让旧代码零改动跑起来
这一招我在实际项目里用得最多。业务代码里到处都是SimHei,一个个改工作量巨大且容易漏,那就用 fontconfig 做一层别名,把SimHei这个名字映射到实际存在的替代字体上。
在~/.config/fontconfig/fonts.conf(或/etc/fonts/conf.d/下)写:
<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <alias binding="same"> <family>SimHei</family> <prefer> <family>Noto Sans CJK SC</family> <family>WenQuanYi Micro Hei</family> </prefer> </alias> <alias binding="same"> <family>SimSun</family> <prefer> <family>Noto Serif CJK SC</family> </prefer> </alias> </fontconfig>binding="same"的意思是“当作同一个字体处理”,这样请求SimHei时如果找不到,就会按prefer列表往下找。prefer的顺序就是你希望的优先级。
写完刷新缓存,然后用fc-match验证:
fc-match SimHei # 期望输出:NotoSansCJK-Regular.ttc: "Noto Sans CJK SC" ...fc-match这条命令几乎是我排查字体问题的第一步,它直接告诉你“系统认为 SimHei 应该用哪个文件来渲染”。输出不对,后面全是白搭。
这套方案的好处是不用动业务代码,坏处是它只在 Linux 上生效,Windows 和 macOS 不吃 fontconfig。所以我的实践是:代码里显式声明 fallback 字体栈,服务器上用 fontconfig 兜底,双保险。
3.4 字体子集化:把 10MB 压到 200KB
只要涉及 Web 加载或者移动端打包,完整字体文件是不可接受的。9 到 10 MB 的 SimHei 放到网页上,用户等字体加载的时间足够泡杯茶了。这时候就要子集化。
原理不复杂:字体文件里大部分字形你根本用不到,中文常用字也就三千多,加上标点、数字、英文,撑死四千个。把用不到的字形剔除,文件体积能降一到两个数量级。
工具首选fonttools里的pyftsubset:
pip install fonttools brotli # 按文本内容提取 pyftsubset SimHei.ttf \ --text-file=used_chars.txt \ --output-file=simhei-subset.woff2 \ --flavor=woff2 \ --layout-features='' \ --no-hinting \ --desubroutinize # 按 Unicode 区段提取(常用汉字 + 基本标点) pyftsubset SimHei.ttf \ --unicodes=U+0020-007E,U+3000-303F,U+4E00-9FFF,U+FF00-FFEF \ --output-file=simhei-cjk.woff2 \ --flavor=woff2几个参数值得说清楚:
--flavor=woff2输出 WOFF2 格式,比原始 TTF 小很多,浏览器全都支持。如果目标是 App 内置或者桌面软件,保留 TTF 就行。
--no-hinting去掉网格拟合指令。在如今的高分屏上,hinting 的价值已经很小了,去掉能再省掉一部分体积。但如果你的使用场景包含低分辨率屏幕或者打印,建议保留。
--layout-features=''去掉 OpenType 特性表。SimHei 本来特性就不多,去不去差别不大,但如果换成思源黑体这种特性丰富的字体,去掉之后能省下可观的体积——代价是失去某些排版能力(比如某些连字、局部位替换)。
used_chars.txt怎么来?我的做法是从项目源码里扫一遍所有中文字符串,或者从语料里统计字频取前 N 个。静态页面直接扫源码最准;动态内容就用常用字表兜底,再把用户可能输入的字符集留出余量。
注意:子集化之后要重新验证
name表有没有被破坏。某些极端参数会导致家族名丢失,结果就是 CSS 里写的font-family匹配不上。用浏览器 DevTools 的 Fonts 面板看一眼实际加载的字体名,是最快的验证方式。
4. 代码侧落地:绘图、PDF、前端三条主线
4.1 数据可视化与图像渲染里的字体指定
matplotlib 是中文方块字最高发的地方。默认配置下它用的是 DejaVu Sans,中文一律显示成方块。正确做法是显式指定字体族,并且关掉 unicode 负号替换——否则负号会显示成方框。
import matplotlib import matplotlib.pyplot as plt matplotlib.rcParams['font.sans-serif'] = [ 'SimHei', # 首选 'Noto Sans CJK SC', # Linux 兜底 'WenQuanYi Micro Hei', # 再兜底 'DejaVu Sans', ] matplotlib.rcParams['axes.unicode_minus'] = False # 关键:让负号正常显示 fig, ax = plt.subplots(figsize=(8, 5)) ax.plot([1, 2, 3], [-1, 0, 1]) ax.set_title('中文标题测试') ax.set_xlabel('横轴') fig.savefig('out.png', dpi=150, bbox_inches='tight')font.sans-serif这个参数接受的是列表,列表里的字体按顺序匹配,找不到就往下走。把这个列表写全,能覆盖大部分环境差异。
改完配置还显示方块的话,八成是字体缓存的问题。matplotlib 会把扫描到的字体缓存到~/.cache/matplotlib/fontlist-*.json,你新装了字体它不一定知道。删掉缓存重启解释器就行:
rm -rf ~/.cache/matplotlib想知道 matplotlib 到底认到了哪些字体,跑这段:
from matplotlib import font_manager for f in font_manager.fontManager.ttflist: if 'Hei' in f.name or 'CJK' in f.name: print(f.name, f.fname)Pillow 那边更直接,直接指定字体文件路径:
from PIL import ImageFont font = ImageFont.truetype("/usr/share/fonts/custom/SimHei.ttf", 32)注意这里用的是路径不是字体名,所以只要路径对就能用,绕开了系统字体匹配的坑。在容器里跑图像处理,我一般都用这种方式,最可控。
4.2 PDF 与文档导出中的嵌入策略
PDF 里字体问题比网页更隐蔽,因为它在一些情况下“看起来正常”,实际却埋了雷。
核心原则:任何对外交付的 PDF,字体必须嵌入。不嵌入的 PDF,在缺字体的机器上打开会被替换成别的字体,轻则跑版,重则乱码。判断方法很简单:
# 安装 poppler-utils 后使用 pdffonts output.pdf输出里会有一列标注是否嵌入(emb yes/no)。如果关键中文显示emb no,这份文件就不要对外发。
不同工具的行为差异很大:
| 工具/方案 | 默认嵌入行为 | 需要注意的点 |
|---|---|---|
| Chromium 无头打印 | 通常嵌入并子集化 | 依赖系统字体,容器内必须先装 |
| WeasyPrint | 嵌入 | 依赖 fontconfig 匹配,别名配置要写对 |
| LaTeX (XeLaTeX) | 嵌入 | 需显式在导言区指定中文字体 |
| 办公软件导出 | 一般嵌入 | 部分版本需手动勾选“嵌入字体” |
| 报表引擎导出 | 视配置而定 | 务必开启子集嵌入 |
用无头浏览器生成 PDF 时,还有个细节:如果你的 HTML 里font-family写的字体在系统里不存在,浏览器会 fallback 到一个默认字体,而这个默认字体很可能不支持中文,结果就是方块。所以容器内的字体安装和 CSS 里的字体栈必须对齐。
LaTeX 场景另说一句:用 XeLaTeX 的话,ctex宏包会自动处理中文字体,但默认可能选的是系统里的某款黑体或宋体。如果你希望输出稳定,最好在导言区显式指定字体文件名,而不是依赖自动匹配。
4.3 前端字体栈与按需加载
前端这一块,我的原则是:中文站点默认不加载中文字体文件,用系统字体栈解决。
原因很实在。一套完整中文字体,即使子集化到常用字,WOFF2 也有一两 MB。这个体积对首屏性能的伤害是实打实的,而收益仅仅是“视觉上更统一一点”,多数场景并不划算。系统字体栈在中文环境下已经能提供不错的效果:
body { font-family: "SimHei", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; }这个栈的思路是:先试目标字体,再依次往下找,最后落到系统默认。注意顺序——如果你确实希望 SimHei 优先(比如要还原某个旧版界面的效果),就把它放最前面;如果只是兜底,就放后面。
真需要自定义字体的时候,用@font-face配合子集和unicode-range做分片加载:
@font-face { font-family: "CustomHei"; src: url("/fonts/hei-basic.woff2") format("woff2"); font-display: swap; unicode-range: U+4E00-9FFF; } @font-face { font-family: "CustomHei"; src: url("/fonts/hei-punct.woff2") format("woff2"); font-display: swap; unicode-range: U+3000-303F, U+FF00-FFEF; }unicode-range让浏览器只下载页面实际用到的分片,font-display: swap保证字体没加载完时先用系统字体顶上,不阻塞渲染。这两个搭配起来,是中文 Web 字体的标准姿势。
移动端 App 内置字体是另一回事。包体积允许的话,内置一份子集化的黑体是值得的——毕竟移动端的系统字体差异比桌面更大,不内置就等着各家机型显示不一致吧。但内置前一定要确认授权允许随应用分发。
5. 排查实录:字体问题的通用定位方法
5.1 三类症状,三种排查路径
字体问题看起来五花八门,实际归类下来就三类症状,每类的排查路径完全不同。
症状一:全变方块(豆腐块)。这是最典型的“字体不存在”。排查顺序是:fc-match 字体名看系统认为该用哪个文件 →fc-list :lang=zh看有没有中文字体 → 检查文件权限和缓存。三步走完,九成问题能定位。容器环境里还要额外确认字体是不是真的进了镜像层,docker run --rm 镜像 fc-list :lang=zh一条命令就能验证。
症状二:中文正常但数字和英文变丑。这是 fallback 的典型表现。你请求的字体里没有拉丁字符(或者拉丁字形质量差),渲染引擎就用另一个字体来补,结果中英文风格割裂、基线不齐。解决办法是显式指定完整的字体栈,让中英文都由同一款字体负责,或者接受混排但把拉丁字体选得好一点。
症状三:粗体不生效或变得很难看。单字重字体的必然结果。渲染器用算法合成伪粗体,笔画会糊在一起。要么换成多字重字体,要么在设计上避免依赖加粗——用字号、颜色、留白来做层级区分,效果往往更好。
还有一种不那么像字体问题的症状,值得单独提一句:文字能显示但间距很怪、标点跑到行首。这通常是字体缺少对应的排版特性表,或者 CSS 的text-spacing、line-break规则与当前字体不匹配。这类问题换字体往往比调 CSS 更快。
5.2 高频问题速查表与避坑清单
下面这张表是我这些年攒下来的,基本覆盖了日常能碰到的绝大多数情况。
| 现象 | 最可能原因 | 快速验证 | 处理办法 |
|---|---|---|---|
| 中文全方块 | 目标环境无中文字体 | fc-list :lang=zh | 安装字体并fc-cache -fv |
| 装了字体仍不生效 | 缓存未刷新或字体名不匹配 | fc-match 字体名 | 删缓存重建,核对name表 |
| 只在新装机器出问题 | 镜像未包含字体 | docker run --rm 镜像 fc-list | 修改 Dockerfile 并加构建期校验 |
| 图表中文方块、英文正常 | 绘图库未指定中文字体 | 打印fontManager.ttflist | 设置rcParams并清缓存 |
| PDF 外发后乱码 | 字体未嵌入 | pdffonts查 emb 列 | 更换导出方式或开启嵌入 |
| 粗体糊成一团 | 单字重字体伪粗体 | 查看字体字重数量 | 换多字重字体或改设计 |
| 偶发生效偶发失败 | 多进程缓存竞态 | 观察是否与并发相关 | 构建期固定字体环境 |
| 字体文件在但程序读不到 | 文件权限不足 | ls -l检查权限 | 统一644/ 目录755 |
再补充几条踩坑经验,这些东西文档里基本不会写:
第一条:不要在运行时下载字体。我见过有项目在服务启动时从网络拉字体文件,网络抖动或者源站变更直接导致服务不可用。字体属于构建产物的一部分,应该在镜像构建阶段就固化进去。
第二条:字体名和文件名要分清。一个文件里可以装多个字体,一个字体名也可以对应多个文件(不同字重)。你按文件名找永远不如按字体名找靠谱。我一般在项目里维护一份字体清单文档,写明家族名、PostScript 名、文件路径、字重、字符覆盖范围,新人接手时不用再猜。
第三条:跨平台测试至少留三套环境。Windows、macOS、Linux 各一套,用同一份测试用例(包含中文、标点、负号、生僻字、中英混排),跑一遍比对输出。这个投入在项目初期看着麻烦,但能避免上线后的返工。
第四条:别在字体上省钱省时间做“临时方案”。“先用着,回头再统一”这句话,我听了太多遍,最后往往就是一年后整个项目里散落着七八种字体声明,没人敢改。字体配置这件事,一开始就做对,成本是最低的。
第五条:授权问题提前问,不要事后补。涉及对外分发字体的项目,在选型阶段就把许可条款拉出来读一遍,确认能不能嵌入、能不能再分发、有没有商用限制。这件事的价值不在于合规本身,而在于它决定了你的技术方案边界——有些字体你看着再合适,用不了就是用不了,早点知道能省下大量返工。
最后说个我自己的习惯:任何新项目初始化时,我会先跑一个字体自检脚本,检查三件事——系统有没有中文字体、我指定的字体家族名能不能被fc-match解析、生成一张含中英文数字标点的测试图看效果。这三步跑通,后面所有的排版和渲染问题都能少一大半。看着是多此一举,但在被方块字坑过几次之后,你会觉得这几分钟的投入实在太值了。