Files
mindfulness/spec_kit/Text Wrap/modules/width-measurement/plan.md
2026-02-10 11:39:33 +08:00

5.6 KiB
Raw Blame History

width-measurement技术计划

1. 计划目标

基于 spec.md设计说明文档/文档换行算法.md v1.2.1,落地 Text Wrap 的“宽度测量与降级approx mode”能力保证

  • APP 场景可进行可靠的文本宽度测量(成熟方案,结果可缓存)
  • WIDGET 场景允许不测量/测量失败时确定性降级EN=wordCountTC=charCount/graphemeCount
  • 缓存 key、错误处理与降级路径固定保证同输入同输出(确定性)
  • fontSpec 缺失字段直接报错(强制调用方补齐,避免隐式默认导致跨端漂移)
  • availableWidth 由“本机设备侧/上层”传入,本模块不内置 widgetProfiles 常量

2. 默认技术决策(本计划采用)

2.1 测量方案(成熟方法)

  • APPRN/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

  • APPAPP|<platform>|<scale?>(按需扩展,但必须固定字段顺序与拼接方式)
  • WIDGETWIDGET|<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 的“长度单位”口径固定为:

  • ENwordCount
  • TCgraphemeCount(优先;若上层仅有 charCount则使用 charCount但必须在实现中写死选择

并必须打点 reason

  • WIDTH_UNKNOWN:没有测量能力/不启用测量
  • MEASURE_FAILED:测量抛错或返回非法值

4. 实现步骤(按落地顺序)

4.1 定义类型与错误

  • 定义 FontSpecContextProfileMeasureResult 类型
  • 定义 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 近似口径写死
  • 单测覆盖缓存命中/隔离与降级路径