☰
ArkUI Search:离线导览索引空白归一与防抖结果快照【鸿蒙心迹】
2026/10/14 1:57:38 网站建设 项目流程

本篇 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 中编译确认。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询