关联:执行-批次4:字体接入 引用规格:规格 11 字体授权与缺失资源降级、规格 09 运行时资产 manifest 本文档只讲怎么验收;改动理由、白名单扩展裁定与取证过程见票 17。
驾驶舱接入 OFL 开源子集字体,并把「字体授权门禁」做成结构上无法绕过的:
| 语义槽位(票 03 锁定,未改名) | 改动前取值(全是系统字体) | 改动后取值 |
|---|---|---|
--font(正文) |
"Microsoft YaHei UI", ... |
"Noto Sans SC Subset", "Microsoft YaHei UI", ...(Web 字体插在系统回退链之前,回退链原样保留) |
--num(数字/等宽) |
"Bahnschrift SemiBold", ... |
"Rajdhani Subset", "Share Tech Mono Subset", "Bahnschrift SemiBold", ... |
关键点:
@font-face,浏览器解析不到该字族 → 自动落到系统回退链。# 1.1 字体自查:证据齐全性 + 门禁分支唯一 + 文件可达与 hash + 回退链 + 对抗式反证
node .scratch/lanhu-cockpit-asset-replacement/tools/check-fonts.mjs
# 1.2 图标 manifest 未被本批破坏(批次 1 的既有断言面)
node .scratch/lanhu-cockpit-asset-replacement/research/check-manifest.mjs
# 1.3 数字度量核对(KPI 不跳格)
set FONTTOOLS_LIBS=<fontTools 安装目录>
python .scratch/lanhu-cockpit-asset-replacement/tools/check-num-metrics.py
# 1.4 产物里没有系统字体
Get-ChildItem public/cockpit/assets/fonts -Recurse -File | Select-Object Name, Length
判定:
字体自查通过(… + 对抗式反证),退出码 0;门禁逐条 5 个 =ok;manifest 校验通过,仍是 18 条 图标(本批未动图标);PASS 未引入新的数字抖动,并列出 16 项逐字差供人工判定;.woff2 + 6 个 licenses/*.txt,没有 msyh* / PingFang / bahnschrift 之类的系统字体文件。| 看什么 | 位置 | 应看到 |
|---|---|---|
| 授权证据 | public/cockpit/assets/fonts/licenses/*.txt |
六份 OFL 1.1 文本,首行是各自 Copyright(Adobe / Indian Type Foundry / Carrois …) |
| 登记事实 | public/cockpit/assets/manifest.json 的 fonts 块 |
licenses 6 条 + entries 5 条;每条含 licenseId、hash、version、holder |
| 构建脚本 | .scratch/…/tools/build-fonts.py |
版本/权利人取自字体二进制与许可证文本,不是手抄 |
为什么要专门说这一点:本批第一版把版本与权利人手写在脚本里,随后被取证推翻——Rajdhani 版本手写成
2.000(实为 1.201)、Share Tech Mono 与 Crimson Text 的权利人张冠李戴。已改为全部从取证源派生:版本读字体name表(ID 5),权利人读归档许可证文本的 Copyright 行(含续行拼接,Adobe 的版权行会折行)。
真实运行地址:http://127.0.0.1:8000/#/cockpit
public/cockpit/assets/manifest.json 的旧转译结果缓存在内存里,模块里根本没有 fonts 块,于是门禁如实地报了「无字体」。这是本次新发现的坑,处置见 §5。.woff2」本身就是「字体已生效」的证据;反之,一条都没有就说明 @font-face 没注入。① 字体真的加载了
woff2,看到 5 条:noto-sans-sc-400 / noto-sans-sc-700 / rajdhani-400 / rajdhani-700 / share-tech-mono-400Content-Type 是 font/woff2(不是 text/html——本仓 dev 会把不存在的路径兜底成 200 + HTML).cockpit-shell 上 data-font-gate="ok"、data-font-faces="5"② 动态文本无豆腐块(本批最关心的一项)
③ 大屏 KPI 数字不跳格
'7' 差 −0.12 em、'-' 差 −0.17 em(0.05 em 以上共 16 项)。代码只保证数字内部等宽性不劣于原字体(Rajdhani 0.203 em vs Bahnschrift 0.236 em,Share Tech Mono 为 0.000 em 真等宽),不假装能判定观感④ 结构未受影响(重点回归)
panelId 与既有断言面不变--font / --num 槽位名未改,只是取值多了前置的 Web 字族// 门禁读数:应为 "ok" / "5"
const shell = document.querySelector('.cockpit-shell')
console.log(shell.dataset.fontGate, shell.dataset.fontFaces)
// 实际生效的字体栈(应看到 Subset 字族排在系统字体之前)
const cs = getComputedStyle(shell)
console.log(cs.getPropertyValue('--font'))
console.log(cs.getPropertyValue('--num'))
// 已加载的字体面
Array.from(document.fonts).map(f => `${f.family} ${f.weight} ${f.status}`)
期望:ok / 5;--font 以 "Noto Sans SC Subset" 开头、--num 以 "Rajdhani Subset" 开头,且后面仍跟着原来的系统回退链;document.fonts 里五个面 status 均为 loaded。
截图留存:总体概览一张(本目录 cockpit-overview-desktop-after-fonts.png)、任一含 KPI 数字的面板特写一张。
体积(子集 vs 原始):
| 字体 | 原始 | 子集后 |
|---|---|---|
| Noto Sans SC Regular | 16.95 MB | 207.2 KB |
| Noto Sans SC Bold | 16.95 MB | 212.2 KB |
| Rajdhani Regular | 0.36 MB | 8.9 KB |
| Rajdhani Bold | 0.38 MB | 9.3 KB |
| Share Tech Mono | 0.04 MB | 8.2 KB |
| 合计 | ≈34.7 MB | 446 KB |
数字度量(对照 --num 原首位 Bahnschrift):
| 字体 | 数字 0–9 内部宽差 | 结论 |
|---|---|---|
| Bahnschrift(旧) | 0.236 em | 基准 |
| Rajdhani 400(新首位) | 0.203 em | 不劣于基准 |
| Rajdhani 700(新) | 0.208 em | 不劣于基准 |
| Share Tech Mono(新次位) | 0.000 em | 真等宽 |
逐字差 > 0.05 em 的字符(16 项,交人工判定):Rajdhani 的 '5' '7' '-' '(' ')'(−0.06 ~ −0.17 em)、'%'(+0.05);Share Tech Mono 的 '1' '.' ':' '/' '(' ')' '%' '-'(−0.15 ~ +0.31 em)。完整表由 check-num-metrics.py 打印。
2026-10-13 维护者人工验收时抓到真身。 维护者反馈「没发现异常,但不确定字体是否被替换回初版」——这个怀疑完全正确:字体确实回退了,而且是静默的。排查后确认有三个互相独立的坑,都会造成同一症状。
现象:manifest.json 已含 fonts 块,但 dev server 返回的转译模块里没有。
根因:public/cockpit/assets/manifest.json 被 runtimeAssets.js 以 import 引用,Vite 会把它转译成 ES 模块并缓存在内存。文件被外部脚本(build-manifest.mjs)改写后,该转译结果可能未失效,页面拿到没有 fonts 块的旧模块。
处置:触碰文件让 watcher 失效缓存,或在 dev server 控制台按 r / 重启:
(Get-Item public/cockpit/assets/manifest.json).LastWriteTime = Get-Date
现象:dev server 转译已含 fonts 块(curl ?import 可见),但页面里字体请求为 0——仍然回退系统字体。
根因(抓包证据):HMR 只热更新了边界组件 CockpitShell.vue?t=...,没有重新请求它传递依赖的 manifest.json 模块。于是浏览器模块图里跑的仍是旧 manifest(无 fonts 块) → resolveFont() 判定不可用 → fontFaces.js 不生成 @font-face → 字体请求 0。
这一类比坑 A 更隐蔽:磁盘对、转译对、代码对,只有浏览器里的模块图是旧的。
处置:硬刷新页面(Ctrl+Shift+R,或浏览器插件的 hard reload),让浏览器重新加载整个模块图。硬刷新后实测字体请求恢复 5/5。
归因更正(2026-10-13 第三轮):我一度判断维护者在看 5173 宿主页——错了,维护者一直开的是独立页
http://127.0.0.1:8000/#/cockpit。本节修复(defaultBase()的 qiankun 注入分支)对将来把驾驶舱嵌入宿主是真实必要的,予以保留;但独立页上「看起来没生效」请按坑 B(旧标签页硬刷新)+ 下方「一句话判定」处理。
场景:驾驶舱被 qiankun 宿主(如 localhost:5173 地下管网管控平台)嵌入时。
根因:@font-face 的 src 与 <img> 的 src 都是相对 URL,浏览器按文档基址解析。微前端下文档是宿主的(localhost:5173),于是 ./cockpit/assets/fonts/... 被解析成 localhost:5173/cockpit/... → 404 到宿主域名。import.meta.env.BASE_URL 在构建期被静态替换为 ./,它不知道运行时被嵌进了哪个宿主。
修复(domain/runtimeAssets.js 的 defaultBase()):qiankun 注入路径优先——
if (scope.__POWERED_BY_QIANKUN__ && scope.__INJECTED_PUBLIC_PATH_BY_QIANKUN__) {
return scope.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
这与本仓 services/cesiumAssets.js 的 pickBase() 同一口径(Cesium 资源在宿主内已验证可用),不是新发明。同一修复顺带覆盖了图标(resolveAsset 共用 defaultBase)——宿主内批次 1 的图标若也有加载失败,根因相同。
验证方式:宿主页面整页刷新(宿主文档重载 → 微应用重新挂载 → 注入路径生效),Network 里字体请求应指向 127.0.0.1:8000/cockpit/assets/fonts/*.woff2(微应用源),不再是 localhost:5173/...(宿主源)。
门禁的设计立场是「无证据字体在解析阶段不可用」——这个立场在数据源陈旧时同样如实生效:manifest 没有 fonts 块,门禁就判定「无字体」,于是不注入 @font-face,页面退回系统字体。门禁是对的,是喂给它的数据陈旧了。
这也是为什么 data-font-gate 读数设计成 fonts-unavailable 而不是别的——它正是为区分「真没字体」与「字体没生效」而存在的。
「字体请求为 0」必须判 FAIL,不能当作「页面没用到字体」放过——因为 --font / --num 里 Web 字族排在首位,页面文本一定用到它们,请求为 0 就是回退了。走查时先看 Network 过滤 woff2 是否有 5 条,再看别的。
| 结果 | 结论 |
|---|---|
| 静态全过 + 走查 ①②④ 全过 | 批次 4 通过,可关闭票 17 |
| ③ 数字观感不可接受 | 记为视觉偏差,登记票 06 缺口清单;可将 --num 回退为不含 Rajdhani |
① 一条 .woff2 都没有 |
缺陷:检查 data-font-gate;若为 fonts-unavailable 先按 §5 失效缓存 |
| ④ 出现模式/面板/地图行为变化 | 阻断:改动越界,需回退并复查 |
# 回滚全部本批改动(含字体文件、manifest 的 fonts 块、tokens 取值、门禁代码)
git checkout -- src/modules/cockpit public/cockpit/assets/manifest.json
git clean -f public/cockpit/assets/fonts
回滚后:--font / --num 恢复纯系统字体栈,页面观感与批次 3 之后一致;图标与位置符号不受影响(资产层改动隔离在 runtimeAssets.js / fontFaces.js / tokens.css 三处)。
--font / --num 的槽位名与语义(票 03 归口)、字号/间距 token(批次 2)、四个一级模式、16 个 panelId、状态词表、地图与定位契约。