ArkUI Search:离线导览索引空白归一与防抖结果快照【鸿蒙心迹】
本篇 Demo 为 QueryHarbor,页面 ArchiveSearchPage,演示任务 QH-2111。文中数据、时间间隔、截图和 HiLog 全部是人为构造的工程示例,未经真实设备、模拟器或输入法兼容测试;既不连接线上检索服务,也不执行 OCR。
一、列表没有坏,坏在输入值被当成了检索值
我做了一个离线导览资料页:打开馆内平面图,旁边放六条简短文字,访客可以按名称查找路线。最初把 ArkUI Search 的输入直接拿去做 includes,看着够用,却很容易在输入法和复制粘贴的细节处出问题。同一个词,有人连续输入“观星”,有人把“观 星”从宣传页粘过来,还有人把前后空格一并复制。页面显示的字差不多,列表却可能一个有结果、一个直接空白。
更糟糕的处理方式,是每次 onChange 都把输入框改写成清洗后的文本。这样看起来可以消掉空格,但编辑过程中的光标、候选词和预上屏文本可能被业务主动回写扰动。对应用来说,输入框中的原文属于用户,真正参与匹配的关键词属于业务,这两份状态不必完全一样。Search 负责输入体验,索引函数负责匹配语义,结果区域只接收一次计算完成的列表快照。
这次我故意不接网络、不接大模型,也不做模糊推荐,只把六条固定词条跑通。因为离线检索一旦出现异常,问题通常不在请求快慢,而在输入规范、更新节奏、状态归属和清理时机。演示任务 QH-2111 的目标很具体:原始输入保持“观 星”,派生关键词为“观星”,最终命中 D-01、D-03、D-06 三条记录。封面只表现这一条本地处理链路。

二、先规定数据合同,避免写完界面才争论搜索规则
演示数据集合固定为六条:D-01“观星台导览”、D-02“河岸夜游”、D-03“观星路线”、D-04“古城工坊”、D-05“城市博物馆”、D-06“观星地图”。这不是现实展馆的数据,ID 只是用于核对稳定性。规则也刻意收窄:删除查询和标题中的空白字符,再做大小写不敏感包含匹配;空字符串返回全部六条。它不承诺拼音、错字纠正、同义词或跨语言分词,避免把工程演示误写成一个完整搜索引擎。
下面这一段负责解决“输入看着相同,匹配却不相同”的问题。业务模型用明确的 id 和 title 字段,结果通过纯函数派生,页面不需要知道空白是从哪一次粘贴进来的。
class ArchiveItem {
id: string;
title: string;
constructor(id: string, title: string) {
this.id = id;
this.title = title;
}
}
const CATALOG: ArchiveItem[] = [
new ArchiveItem('D-01', '观星台导览'),
new ArchiveItem('D-02', '河岸夜游'),
new ArchiveItem('D-03', '观星路线'),
new ArchiveItem('D-04', '古城工坊'),
new ArchiveItem('D-05', '城市博物馆'),
new ArchiveItem('D-06', '观星地图')
];
function normalizeQuery(raw: string): string {
return raw.replace(/\s+/g, '').toLowerCase();
}
function findLocal(raw: string): ArchiveItem[] {
const key: string = normalizeQuery(raw);
if (key.length === 0) {
return CATALOG.slice();
}
return CATALOG.filter((item: ArchiveItem) =>
normalizeQuery(item.title).includes(key));
}
函数没有定时器、权限、数据库句柄,也不持有 UI 生命周期资源;给同一批资料和同一文本就应该返回同一结果。这里的“删除空白”属于演示场景的业务决定,不应直接照搬到书名、地名的所有语言中:英文多词短语、空格有语义的编号和特殊搜索语法,都需要单独设计规范。实际产品还应考虑全角半角、Unicode 归一化和用户所处语言环境,但不能未经评估就用一个正则覆盖所有情况。
三、让输入回调保持轻量,防抖的是计算,不是键盘
第二个问题是输入过程中每敲一个字就重算页面。六条数据当然不会卡,但同一写法扩展到上万条本地资料时,毫无节制的匹配会增加主线程负担。这里设置 180 ms 的演示防抖窗口:新文本到达先取消上次预约,再安排下一次计算。点击键盘上的搜索按钮则不等待窗口,直接提交当前文本。这里的“提交”不是写入数据库,只是让当前搜索立即生效。
状态区分为 inputValue、normalizedKey、results 和 message。前者严格保留 Search 的显示文本,后两者是上一次完成计算的快照。即使定时器已经排队,也用 generation 拦截旧回调;它只是一个页内运行代次,不是网络请求编号。下段负责计时、状态转换与生命周期收口,不依赖实际输入法的某种事件顺序。
@Entry
@Component
struct ArchiveSearchPage {
@State inputValue: string = '观 星';
@State normalizedKey: string = '观星';
@State results: ArchiveItem[] = findLocal('观 星');
@State message: string = '3 条 · 已更新';
private timerId: number = -1;
private generation: number = 0;
private alive: boolean = true;
aboutToAppear(): void { this.alive = true; }
private cancelPending(): void {
if (this.timerId >= 0) {
clearTimeout(this.timerId);
this.timerId = -1;
}
}
private applySnapshot(raw: string): void {
this.normalizedKey = normalizeQuery(raw);
this.results = findLocal(raw);
this.message = `${this.results.length} 条 · 已更新`;
console.info('QH-2111 SNAPSHOT key=' + this.normalizedKey +
' count=' + this.results.length);
}
private onTyping(raw: string): void {
this.inputValue = raw; // 不把归一结果写回输入框
this.cancelPending();
const token: number = ++this.generation;
this.message = '等待输入结束';
this.timerId = setTimeout(() => {
this.timerId = -1;
if (!this.alive || token !== this.generation) { return; }
this.applySnapshot(raw);
}, 180);
}
private onSearch(raw: string): void {
++this.generation;
this.cancelPending();
this.inputValue = raw;
this.applySnapshot(raw);
}
aboutToDisappear(): void {
this.alive = false;
++this.generation;
this.cancelPending();
}
取消定时器和拦截代次是两层防线:前者减少无用工作,后者防止退出页面后的旧闭包继续更新已失效的状态。这里没有申请系统资源,也没有所谓“关闭搜索接口”可调用;需要释放的就是页面持有的计时器。若接入后台任务或数据库查询,取消信号还必须延伸到相应任务,不能认为 clearTimeout 可以取消一个已经发出去的异步查询。工程里容易忽略的一点是,计时器被取消不等于结果被清空,在下一次快照完成前旧结果会短暂保留,状态文案因此必须明确显示“等待输入结束”。
四、组件只读快照,避免输入框与列表互相回写
布局部分使用系统 Search 承接输入事件。onChange 接收用户当前文本,不做光标控制、不在回调中删除空白;onSubmit 接收确认文本并立即刷新。列表用稳定的 id 作为 ForEach 键,文本渲染只读取 results,不再根据输入框文本进行第二次筛选。这样截图、示例日志与用户看到的内容都来自相同快照,而不是各自独立推算。
下面是 ArchiveSearchPage 的 build 部分,与前面两个代码块放在同一个 .ets 文件中,构成便于教学使用的最小页内示例。前一段结构体尚未闭合,结尾大括号位于本段;原始资料数组放在页面结构体之外。
build() {
Column({ space: 14 }) {
Text('QueryHarbor · QH-2111').fontSize(16)
Text('ArchiveSearchPage').fontSize(24).fontWeight(FontWeight.Bold)
Search({ value: this.inputValue, placeholder: '搜索离线导览' })
.width('94%')
.onChange((value: string) => this.onTyping(value))
.onSubmit((value: string) => this.onSearch(value))
Text('归一关键词:' + this.normalizedKey)
Text(this.message).fontColor('#4B7B70')
List() {
ForEach(this.results, (item: ArchiveItem) => {
ListItem() {
Row({ space: 12 }) {
Text(item.id).fontColor('#66758C')
Text(item.title).fontSize(17)
}.padding(12)
}
}, (item: ArchiveItem) => item.id)
}.width('94%').layoutWeight(1)
Text('演示界面 · 非真机测试').fontSize(12)
}.width('100%').height('100%').padding(16)
}
}
在真实页面中,Search 的 onChange 还可能遇到输入法预上屏阶段,官方文档也提示开发者在光标操作时要考虑预上屏参数。当前实现刻意不控制光标,因此不宣称已经完成所有输入法适配。若用户会输入很长的正则式或检索表达式,应该另建解析层,不让 includes 承担它不具备的语义。列表行复用和滚动位置还需在更大数据量下验收,六条示例只用于验证状态边界。

五、验收看可重复的输出,不凭演示截图猜测效果
演示输入框保留 “观 星” 三个可见字符(中间有一个空格),规则将空白删除后得到 “观星”。六条记录中,D-01“观星台导览”、D-03“观星路线”、D-06“观星地图”命中;另外三条不应混入。对应状态为“3 条 · 已更新”。示例 HiLog 约定为 QH-2111 SNAPSHOT key=观星 count=3,这是和源码一致的预期日志字符串,不是声称在 DevEco 中已经观察到的真机输出。
验收应按顺序做四组操作:先清空文本,期望六条全部出现;再输入“观 星”,等 180 ms 后确认三个稳定 ID;然后在计时尚未结束时连续改为其他词,确保只提交最后一个快照;最后在等待期间退出页面,确认计时器已取消,页面不会再被旧回调写入。以上是建议的手动验收项,不是本文已执行的自动化测试。截图特意保留顶部时间、电量和状态图标,只表明设计目标是手机上的纯屏幕视图,不证明系统状态栏由应用控制。

六、把几类容易漏掉的边界提前写进任务单
第一类是语言边界。 空白折叠不是天然正确:例如“New Town”如果被拼成“newtown”,未必等价于词组查询。业务若允许英文短语,要定义分词、索引和大小写处理规范;如果只有馆内固定导览词条,则可以像本例一样在范围声明中写清楚规则。单测应覆盖全角空白、制表符和空文本,而不能只测普通空格。
第二类是数据边界。 如果后台后来更新了资料,原来的 results 快照不应该偷偷反映一半新数据、一半旧数据。需要在主数据更新后重新计算,或通过索引版本号协调。此处 CATALOG 是不可变演示常量,所以问题被人为排除;扩展时必须补上版本和快照一致性策略。ID 也应由数据源提供,而不是随列表排序改变的行号。
第三类是性能边界。 180 ms 只是交互方案中的样例值,不能凭感觉认定最优。真实设备上要对大数据集合做耗时统计,必要时改为预索引、分批计算或工作线程;结果若来自网络,应再加请求取消和回包代次过滤。防抖不会缩短一次昂贵计算的实际耗时,更无法替代错误处理和用户可见的加载反馈。
第四类是页面退场。 aboutToDisappear 只做这个 Demo 持有资源的清理,不代表应用进程退出时所有异步操作都自动被取消;如果未来把词库写入持久化介质,还要独立考虑写入原子性和失败恢复。清空输入时应确保旧的“无结果”提示一起被替换,不能只把搜索框视觉上擦掉。至于日志,建议只打印归一后的测试关键词,真实产品遇到隐私敏感检索应进一步限制记录范围。
七、留下的不是万能搜索框,而是清楚的状态分界
这一版 QueryHarbor 的价值,不在于做出了三个搜索结果,而在于把输入原文、规范化键、计算中的状态、已落地的结果快照拆成了四件事。代码读起来甚至有点啰嗦,却能让“搜不到”“列表闪一下”“返回后还在改数据”被分开定位。后续无论换成文搜图接口、离线全文索引还是联网搜索,都可以复用这个划分,再为每一种真实外部能力补上自己的取消、异常和权限策略。本文所有图片是演示重绘;本轮没有运行真实检索、输入法兼容测试或性能压测。
参考资料:华为 HarmonyOS 官方《Search》组件 API 参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-search。本文核对了 Search 初始化选项、onChange、onSubmit 的基本定义;未在目标 SDK 中编译确认。
更多推荐





所有评论(0)