139 lines
5.6 KiB
Markdown
139 lines
5.6 KiB
Markdown
# width-measurement(技术计划)
|
||
|
||
## 1. 计划目标
|
||
|
||
基于 `spec.md` 与 `设计说明文档/文档换行算法.md v1.2.1`,落地 Text Wrap 的“宽度测量与降级(approx mode)”能力,保证:
|
||
|
||
- APP 场景可进行**可靠的文本宽度测量**(成熟方案,结果可缓存)
|
||
- WIDGET 场景允许不测量/测量失败时**确定性降级**(EN=wordCount,TC=charCount/graphemeCount)
|
||
- 缓存 key、错误处理与降级路径固定,保证**同输入同输出**(确定性)
|
||
- `fontSpec` 缺失字段直接报错(强制调用方补齐,避免隐式默认导致跨端漂移)
|
||
- `availableWidth` 由“本机设备侧/上层”传入,本模块不内置 widgetProfiles 常量
|
||
|
||
## 2. 默认技术决策(本计划采用)
|
||
|
||
### 2.1 测量方案(成熟方法)
|
||
|
||
- **APP(RN/Expo)**:采用成熟的“离屏文本测量”库实现 `measureWidth(text, fontSpec)`
|
||
- 建议选型:`react-native-text-size`(或团队已有的等价成熟方案)
|
||
- 理由:可直接测量给定字体参数下的文本宽度,避免用 UI 渲染 onLayout 造成异步/不确定性
|
||
|
||
> 说明:本计划把“测量实现”作为本子模块交付的一部分,而不是仅定义接口;但仍保留注入点,便于替换实现或做平台差异适配。
|
||
|
||
### 2.2 缓存策略(由我统一定义)
|
||
|
||
采用“两级缓存 + 有上限”的策略:
|
||
|
||
1. **字符串测量缓存**:`(text, fontSpecKey, contextProfile) -> width`
|
||
2. **切片测量缓存**:`(start, end, fontSpecKey, contextProfile) -> width`
|
||
|
||
并且:
|
||
|
||
- 缓存采用**模块级常驻缓存**(跨 `wrapText()` 多次调用复用),提升性能
|
||
- 缓存容量必须有上限(建议 LRU 或“超限清空 + 打点”),避免内存无限增长
|
||
|
||
### 2.3 fontSpec 严格性(缺失就报错)
|
||
|
||
`fontSpec` 在 APP 测量路径下必须包含:
|
||
|
||
- `fontSize`
|
||
- `fontFamily`
|
||
- `fontWeight`
|
||
|
||
缺失任意字段时:
|
||
|
||
- **直接抛错**(错误信息必须为简体中文,并指出缺失字段与调用方应补齐的位置)
|
||
- 不允许在测量层做“隐式默认值”(避免跨端不一致与线上难定位)
|
||
|
||
### 2.4 contextProfile 的口径
|
||
|
||
为保证缓存与确定性,本模块定义 `contextProfile`:
|
||
|
||
- APP:`APP|<platform>|<scale?>`(按需扩展,但必须固定字段顺序与拼接方式)
|
||
- WIDGET:`WIDGET|<widgetSize?>`(如果上层传入 widgetSize,可写入;否则仅 WIDGET)
|
||
|
||
> 注意:你已确认 `availableWidth` 由设备侧传入,本模块不内置 widgetProfiles;但缓存仍需要一个 profile 字段区分 APP/WIDGET。
|
||
|
||
## 3. API 设计(实现阶段稳定契约)
|
||
|
||
### 3.1 对外接口(建议)
|
||
|
||
- `buildFontSpecKey(fontSpec) -> string`
|
||
- `measureWidthCached({ text, context, fontSpec, contextProfile }) -> { width: number | null; meta: { isApprox: boolean; reason?: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED' } }`
|
||
- `measureSliceWidthCached({ tokens, start, end, joinTokens, ... }) -> { width: number | null; meta: ... }`
|
||
|
||
### 3.2 approx mode 口径(必须确定性)
|
||
|
||
当出现任一情况时进入 approx mode(返回 `width=null`,并由上层按 approx 口径计算 lineLen):
|
||
|
||
- `measureWidth` 不可用
|
||
- `measureWidth` 抛错
|
||
- `measureWidth` 返回 NaN/Infinity/负数
|
||
- context=WIDGET 且上层选择不测量
|
||
|
||
approx 的“长度单位”口径固定为:
|
||
|
||
- EN:`wordCount`
|
||
- TC:`graphemeCount`(优先;若上层仅有 charCount,则使用 charCount,但必须在实现中写死选择)
|
||
|
||
并必须打点 reason:
|
||
|
||
- `WIDTH_UNKNOWN`:没有测量能力/不启用测量
|
||
- `MEASURE_FAILED`:测量抛错或返回非法值
|
||
|
||
## 4. 实现步骤(按落地顺序)
|
||
|
||
### 4.1 定义类型与错误
|
||
|
||
- 定义 `FontSpec`、`ContextProfile`、`MeasureResult` 类型
|
||
- 定义 `MissingFontSpecError`(或统一错误码),错误信息简体中文
|
||
|
||
### 4.2 实现 fontSpecKey
|
||
|
||
实现 `fontSpecKey = fontFamily|fontWeight|fontSize`:
|
||
|
||
- 字段顺序固定
|
||
- `fontSize` 转为字符串(禁止浮点格式漂移:建议 `String(fontSize)`,并要求输入是 number 且有限)
|
||
|
||
### 4.3 实现缓存容器
|
||
|
||
- 实现一个带上限的缓存(LRU 优先;若不引入依赖,先用 Map + 超限清空)
|
||
- Key 生成必须确定性:
|
||
- `textKey = <contextProfile>|<fontSpecKey>|<text>`
|
||
- `sliceKey = <contextProfile>|<fontSpecKey>|<start>|<end>`
|
||
|
||
### 4.4 接入成熟测量库(APP)
|
||
|
||
- 封装 `measureWidthImpl(text, fontSpec) -> number`
|
||
- 对返回值做校验(有限、非负)
|
||
- 失败捕获并走 approx mode(并打 `MEASURE_FAILED`)
|
||
|
||
### 4.5 WIDGET 策略
|
||
|
||
- 默认允许调用方传入 `measureWidthImpl`(如果 Widget 侧实现了测量)
|
||
- 若不传/不启用:直接 approx mode,并 reason=`WIDTH_UNKNOWN`
|
||
|
||
## 5. 测试计划(Vitest)
|
||
|
||
### 5.1 纯函数与缓存测试(必须)
|
||
|
||
- `fontSpecKey`:同输入同输出;缺字段抛错
|
||
- 缓存命中:同 key 不重复调用底层 `measureWidthImpl`
|
||
- 缓存隔离:不同 `contextProfile/fontSpecKey` 不互相污染
|
||
|
||
### 5.2 降级路径测试(必须)
|
||
|
||
- 缺少 `measureWidthImpl` -> approx mode + reason=`WIDTH_UNKNOWN`
|
||
- `measureWidthImpl` 抛错/返回 NaN -> approx mode + reason=`MEASURE_FAILED`
|
||
|
||
> 说明:测量库本身的准确性不在单测中做像素级断言;单测只验证“缓存与降级语义确定性”。
|
||
|
||
## 6. 完成定义(DoD)
|
||
|
||
- APP/WIDGET 下 `width` 返回语义清晰:可测量返回 number,不可测量返回 null
|
||
- `fontSpec` 缺字段必定报错(简体中文错误信息)
|
||
- 缓存 key 与容量策略确定性,且有上限
|
||
- approx mode 触发条件、reason 标记、EN/TC 近似口径写死
|
||
- 单测覆盖缓存命中/隔离与降级路径
|
||
|