rawfile 资源与强类型词库加载器:schema / data / source 三层版本

"开口练"的核心能力------本地检测填充词、犹豫词、笼统词------建立在三个 JSON 词库上:实时词库(16 填充 + 14 犹豫 + 20 笼统)、情感词库(146 词)、分层候选词库(9 组 97 条)。这批数据有几个硬约束:

  • 原版产品(JS 实现)已在用,行为必须 1:1,一个词都不能差;

  • 离线可用是产品卖点,不能依赖网络下发

  • 词库会迭代,但迭代节奏和代码发布不同步,版本要能独立演进

结论:打进 HAP 的 rawfile,配上严格的版本契约和加载校验。这篇讲这套机制。

1. 为什么是 rawfile

鸿蒙给静态数据几条路:写死在 ArkTS 常量里、放 resources/base/element/ 资源文件、放 resources/rawfile/、网络下发。逐个排除:

  • 代码常量:数据与代码同编译,改一个词要全量构建;且几百个词条混在代码里,数据 diff 没法审。

  • element 资源:面向字符串/颜色/尺寸这类"会被系统按配置(语言、深浅色)解析"的资源,词库 JSON 不是这个语义。

  • 网络下发:冷启动引入网络依赖,离线卖点直接破产。

  • rawfile :任意格式文件原样打进 HAP,运行时 ResourceManager 按文件名读字节------正解。

文件布局,命名带项目统一的 sl_ 前缀:

复制代码
entry/src/main/resources/rawfile/
├── sl_realtime_lexicon.json
├── sl_emotion_lexicon.json
└── sl_tiered_lexicon.json

注意路径是 resources/rawfile/ 而不是 resources/base/rawfile/------rawfile 不参与 base/dark 那套配置解析,直接放 resources 根下。

2. 三层版本号:schema、data、source 各管一件事

每个词库 JSON 的顶层是一个"信封",带三个版本号:

复制代码
{
  "schemaVersion": "1.0.0",
  "dataVersion": "2026.07.14",
  "sourceVersion": "expression-trainer@1.0.0",
  "payload": { }
}

三个版本回答三个不同的问题:

版本 回答的问题 代码怎么处理
schemaVersion 我的结构你还能读懂吗 semver 校验 + 主版本兼容性检查,主版本不符直接拒绝加载
dataVersion 这批数据是哪一版 透传进 Catalog,供日志/报告标注"本次分析基于哪版词库"
sourceVersion 数据从哪个上游移植来 溯源凭证,对着原版产品核对行为基线时用

schema 检查是硬门槛:

复制代码
const SUPPORTED_SCHEMA_MAJOR = '1';

function assertSupportedMajor(version: string, file: string): void {
  const major = version.split('.')[0];
  if (major !== SUPPORTED_SCHEMA_MAJOR) {
    throw lexErr(
      SpeakLabLexiconErrorCode.UNSUPPORTED_SCHEMA_VERSION, file,
      `不支持的 schemaVersion 主版本 "${major}",当前只支持 "${SUPPORTED_SCHEMA_MAJOR}"`
    );
  }
}

只检查主版本是刻意的:minor/patch 演进必须向后兼容(加字段不删字段),主版本变了才允许破坏性格式调整------届时旧版 App 拒绝加载新词库,而不是读错结构默默算出错误结果。数据格式契约和 API 契约是同一个道理。

3. Parser:零 I/O 的纯函数,校验严到"计较旧拼写"

解析层和 I/O 层严格分离。SpeakLabLexiconParser 文件头写着它的姿态:接受文本/字节;不含 I/O、UI、ASR 或 AI 依赖。失败时抛异常,绝不返回部分结果。

纯函数解析器的好处是测试可以脱离设备------Node 脚本拿同样的 JSON 喂同样的契约跑 oracle 对拍(B08 讲算法时细说),Hypium 里也能直接构造非法文本断言各种错误码。

校验强度远超"JSON.parse 不炸就行":

固定计数基线写进代码。词库词条数不是"读出来多少算多少",而是冻结的行为基线:

复制代码
const REALTIME_FILLER_COUNT = 16;
const REALTIME_HEDGE_COUNT = 14;
const REALTIME_VAGUE_COUNT = 20;
const EMOTION_COUNT = 146;
const TIERED_GROUP_COUNT = 9;
const TIERED_TOTAL_COUNT = 97;

解析后逐组核对:分层词库 9 个分组的名字、顺序、每组条目数全部固定,连每条候选的词数(6 个)都是常量。少一个词、组序错了,都是加载失败。这批数字就是原版产品的行为基线,代码把它们变成运行时断言------词库错了不是"数据差一点",是构建事故,必须当场炸

连历史拼写错误都显式处理 。原版数据里有个字段拼成了 vagueToPresice(正确应为 vagueToPrecise)。Parser 显式识别这个旧拼写并完成迁移,而不是把错误拼写传染进新代码:

复制代码
/** 旧拼写(已显式迁移为 vagueToPrecise)。 */
const STALE_FIELD_SPELLING = 'vagueToPresice';

还有个 ArkTS 特有的小坑值得一提:ArkTS 不支持 in操作符 ,检测"JSON 对象有没有这个键"得用 Object.keys 遍历:

复制代码
/** ArkTS 不支持 in 操作符;改用 Object.keys 检测键存在性。 */
function hasKey(obj: Record<string, Object>, key: string): boolean {
  const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) { if (keys[i] === key) return true; }
  return false;
}

从 JS 移植数据解析代码时,这类语言差异点是最容易漏的。

4. Repository:I/O 收口与分层降级

I/O 层 SpeakLabLexiconRepository 只做两件事:读字节、定降级策略。

读取本身是 ResourceManager + TextDecoder 两行核心:

复制代码
async function readRawfile(mgr: resourceManager.ResourceManager, name: string): Promise<string> {
  try {
    const bytes: Uint8Array = await mgr.getRawFileContent(name);
    const decoder = new util.TextDecoder('utf-8');
    return decoder.decodeToString(bytes);
  } catch (e) {
    throw new SpeakLabLexiconError(SpeakLabLexiconErrorCode.IO_ERROR, name, `...`);
  }
}

真正的设计在降级策略(ADR-003 冻结):三个词库的可选性不一样,容错必须分层

复制代码
sl_realtime_lexicon.json  必需  → 失败 = 整体初始化失败,抛异常
sl_emotion_lexicon.json   辅助  → 失败 = EMOTION_DEGRADED,realtime 照常可用
sl_tiered_lexicon.json    辅助  → 失败 = TIERED_DEGRADED,realtime 照常可用
两者均失败                       → AUXILIARY_DEGRADED

代码把策略写得很直白:

复制代码
let emotionOk = false;
try {
  emotion = parseEmotionCatalog(await readRawfile(mgr, FILE_EMOTION), FILE_EMOTION);
  emotionOk = true;
} catch (_e) {
  // 情感词库降级:realtime 保留
}

两个方向的红线都在这里:

  • 不用空词库伪装成功。realtime 失败时宁可整体拒绝服务,也不能装一份空词库让分析"成功"地什么都检测不出来------那是拿错误结果冒充正确结果。

  • 降级是显式状态,不是静默吞错。辅助词库失败会写进 Catalog 的 availability 字段,上层 UI 可以据此提示"情感建议暂不可用",日志里也有明确事件。静默 catch 和显式降级,区别是后者能被看见、被测试、被追责。

加载时机上,这套加载挂在 B03 讲的启动就绪闸门里:首屏渲染前 ensureSpeakLabShellCompositionReady 完成词库加载,就绪 promise 进程级去重、失败可重试。词库就绪后设置里的自定义词 overlay 才有附着点。

5. 小结

  • 静态业务数据随包走 rawfile:resources/rawfile/,命名带统一前缀,ResourceManager + TextDecoder 读取。

  • 三层版本号各司其职:schema 管兼容性(主版本硬门槛)、data 管数据迭代、source 管移植溯源。

  • Parser 是零 I/O 纯函数:固定计数基线即行为断言,宁炸不糊弄;历史拼写显式迁移;注意 ArkTS 无 in 操作符。

  • 降级分层:必需数据失败=整体失败,辅助数据失败=显式 availability 降级;不用空数据伪装成功,不静默吞错。

相关推荐
程序员黑豆3 小时前
鸿蒙应用开发:Grid组件实现九宫格布局教程
前端·华为·harmonyos
程序员黑豆4 小时前
鸿蒙应用开发:Flex 组件从入门到实战
前端·华为·harmonyos
<小智>4 小时前
鸿蒙多功能工具箱开发实战(二十)-性能优化与打包发布
ui·华为·harmonyos
不肥嘟嘟右卫门6 小时前
鸿蒙原生ArkTS布局方式之Popup+TextInput提示输入布局深度解析
华为·harmonyos
nullregedit7 小时前
原生鸿蒙像素画板实战 22:快捷键与鼠标交互
harmonyos·arkts·鸿蒙·bitart·像素画
●VON8 小时前
鸿蒙 PC Markdown 编辑器图片粘贴:从系统剪贴板到标准相对链接
华为·编辑器·harmonyos·鸿蒙
●VON8 小时前
鸿蒙 PC Markdown 编辑器外部修改检测:从文件指纹到冲突决策
华为·编辑器·harmonyos·鸿蒙
●VON8 小时前
鸿蒙 PC Markdown 编辑器十兆级大文档保护模式
华为·编辑器·harmonyos·鸿蒙
●VON8 小时前
鸿蒙 PC Markdown 编辑器三方冲突处理:本地缓冲区、磁盘版本与共同基线
网络·华为·编辑器·harmonyos·鸿蒙