Search 的 ArkUI 原生实践

实现范围:HarmonyOS 原生 ArkTS / ArkUI

文章依据当前 Index.ets、默认运行图和操作后运行图整理;所有判断以页面可观察结果为准。

摘要

Search 被放在 ArkUI 基础组件 的语境中讨论。本文关注的不是把控件堆到页面上,而是让默认状态、用户或系统输入、状态更新和可见反馈形成可检查的关系。当前页面通过 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 展示任务,通过 页面同时包含 7 个点击入口和 1 个值变化入口 提供变化入口,并用代码与两张运行图保留验证证据。

可观测性不只存在于日志里

文章中的运行图、操作后运行图和代码图共同构成一组轻量证据。初始图证明默认状态,操作图证明状态确实改变,代码图说明改变来自哪段 ArkTS 实现。对于 Search,这三者需要围绕同一个动作组织:读者应能从初始图看到操作入口,从操作图看到结果差异,再从代码中找到对应的状态和回调。

如果把页面接入到更复杂的工程,还需要补充日志、耗时、错误码和必要的埋点。记录时应避免写入敏感内容,并把事件名称、输入类别、结果类别和耗时分开保存。这样排查时可以先判断失败集中在哪个分支,再回到页面检查用户是否被正确告知。日志是排查工具,界面反馈是产品行为,两者不能相互替代。

页面初始运行状态

测试要覆盖“动作之后”

只验证应用能启动,无法证明 Search 的状态链正确。最小回归集至少包括:默认启动一次;关键操作一次;边界操作一次;重复操作一次;异常或缺失数据一次。每个用例都需要预期一个可见结果,而不是只期望“不崩溃”。例如,默认态应显示 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 中能够识别任务的内容;操作后应改变对应文本、选中态、数值或图形;失败态应保留可读信息。

自动化测试可以将状态更新逻辑与组件渲染分开验证。先对输入到状态的转换做单元测试,再对关键按钮、输入框或导航入口做 UI 测试,最后在真机或模拟器检查布局与系统能力。这样分层后,某次失败可以被归类为计算错误、事件接线错误、渲染错误或环境差异,不必把所有问题都归到页面组件上。

性能与资源成本需要有边界

页面规模不大时,性能问题通常不明显;但当 Search 的数据频率提高、列表条目增多、图片或媒体资源接入后,重复构建和重复 I/O 会成为可测量的成本。先从状态更新范围看:一次操作是否只改变依赖它的区域;是否在回调中重复创建大对象;是否把频繁变化的数据直接传给整个页面树。再从资源角度看:图片、音视频、数据库游标和订阅是否有明确释放时机。

性能优化需要以可测量现象为起点。可以记录首屏时间、一次交互后的响应时间、列表滚动帧率、内存波动或网络重试次数。没有现象时不应为了“看起来优化”而拆散状态和组件;有现象时也不要只凭感觉改代码。先确定复现步骤、采样设备和指标口径,再验证优化是否改变了用户可感知的结果。

运行证据与核对口径

核对对象 当前页面中的证据 发布前应确认的结果
默认页面 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 首屏有标题、说明和主要内容,不能是空白或错误页。
可操作入口 页面同时包含 7 个点击入口和 1 个值变化入口 执行一次操作后,至少一处可见文字、选中态、数据或布局发生改变。
状态来源 State query、State submitted、State history、State resultNames 每个页面结果都能追溯到一个明确输入、回调或初始化分支。
源码对应关系 Index.ets 与代码截图 运行图、代码图和正文描述来自同一版实现。
异常回退 组件属性、尺寸、选中态与触控反馈 输入无效、能力不可用或数据缺失时仍能给出可读结果。

可访问性与文案是交互契约的一部分

Search 的主入口需要有可辨识文字,不能仅依赖颜色、图标或位置传达状态。当前页面中的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 是读者理解页面任务的线索;当状态变化后,结果文本也应说明发生了什么,而不是只换一个难以解释的色块。对于屏幕阅读、字体放大或触控范围较小的用户,标题、控件名称、禁用原因和错误提示都属于功能的一部分。

实际检查时可以放大系统字体、切换深浅色、使用较小屏幕并连续触发操作。若信息被截断,应优先调整容器与文本策略;若误触频繁,应检查操作区尺寸与间距;若状态只靠颜色区分,应补充文字或图标。把这些检查写进发布前清单,比在问题出现后只修一张截图更可靠。

当前 ArkTS 实现与阅读顺序

下面的源码是 Search 页面当前使用的 Index.ets。阅读时可以先看状态声明,再看组件树,最后看事件回调。这样能避免把一段 UI 声明误读成完整业务流程。页面同时包含 7 个点击入口和 1 个值变化入口,这些入口是运行图中状态改变的直接证据。

const PRODUCTS: string[] = ['无线蓝牙耳机', '降噪头戴耳机', '智能手表 Pro', '运动手环 6', '轻便跑步鞋', '人体工学椅'];
const PRICES: string[] = ['¥299', '¥899', '¥1299', '¥199', '¥399', '¥1599'];

@Entry
@Component
struct Index {
  @State query: string = '';
  @State submitted: boolean = false;
  @State history: string[] = ['耳机', '运动'];
  @State resultNames: string[] = [];

  private search(keyword: string): void {
    const value = keyword.trim();
    if (value.length === 0) { this.submitted = false; return; }
    this.query = value;
    this.resultNames = PRODUCTS.filter((item: string) => item.includes(value));
    this.history = [value, ...this.history.filter((item: string) => item !== value)].slice(0, 5);
    this.submitted = true;
  }

  private priceOf(name: string): string {
    return PRICES[PRODUCTS.indexOf(name)];
  }

  @Builder
  ProductRow(name: string) {
    Row({ space: 12 }) {
      Text(name.substring(0, 1)).fontSize(19).fontWeight(FontWeight.Bold).fontColor(Color.White).width(44).height(44).textAlign(TextAlign.Center).padding({ top: 10 }).backgroundColor('#2563EB').borderRadius(14)
      Column({ space: 3 }) { Text(name).fontSize(16).fontWeight(FontWeight.Medium); Text(PRODUCTS.indexOf(name) < 4 ? '数码 · 热门推荐' : '生活 · 品质精选').fontSize(12).fontColor('#667085') }.alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text(this.priceOf(name)).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#2563EB')
    }.width('100%').padding(14).backgroundColor(Color.White).borderRadius(16)
  }

  build() {
    Scroll() {
      Column({ space: 14 }) {
        Row() { Column({ space: 4 }) { Text('商品搜索').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#172033'); Text('实时联想 · 历史记录 · 商品结果').fontSize(13).fontColor('#667085') }.alignItems(HorizontalAlign.Start).layoutWeight(1); Text('006').fontSize(13).fontColor('#2563EB').padding(10).backgroundColor('#EAF1FF').borderRadius(12) }.width('100%')
        Row({ space: 8 }) {
          TextInput({ placeholder: '搜索商品,如:耳机 / 运动', text: this.query }).layoutWeight(1).height(48).backgroundColor(Color.White).borderRadius(24).onChange((value: string) => { this.query = value; this.submitted = false; })
          Button('搜索').height(48).backgroundColor('#2563EB').borderRadius(14).onClick(() => { this.search(this.query); })
        }.width('100%')
        if (!this.submitted && this.query.length === 0) {
          Column({ space: 12 }) {
            Row() { Text('搜索历史').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1); Text('清空').fontSize(13).fontColor('#D92D20').onClick(() => { this.history = []; }) }.width('100%')
            if (this.history.length === 0) { Text('暂无历史记录').fontSize(14).fontColor('#98A2B3').width('100%') } else { ForEach(this.history, (item: string) => { Text('⌕  ' + item).fontSize(15).fontColor('#475467').width('100%').padding(12).backgroundColor('#F1F5F9').borderRadius(10).onClick(() => { this.search(item); }) }, (item: string) => item) }
            Text('热门搜索').fontSize(18).fontWeight(FontWeight.Bold).width('100%').margin({ top: 4 })
            Row({ space: 8 }) { Text('耳机').fontSize(14).padding(10).backgroundColor('#EAF1FF').borderRadius(10).onClick(() => { this.search('耳机'); }); Text('运动').fontSize(14).padding(10).backgroundColor('#ECFDF3').borderRadius(10).onClick(() => { this.search('运动'); }); Text('家居').fontSize(14).padding(10).backgroundColor('#FFF7ED').borderRadius(10).onClick(() => { this.search('椅'); }) }.width('100%')
          }.width('100%').padding(16).backgroundColor(Color.White).borderRadius(18)
        } else if (!this.submitted) {
          Column({ space: 8 }) { Text('搜索联想').fontSize(18).fontWeight(FontWeight.Bold).width('100%'); ForEach(PRODUCTS.filter((item: string) => item.includes(this.query)).slice(0, 4), (item: string) => { Text('⌕  ' + item).fontSize(15).width('100%').padding(12).backgroundColor(Color.White).borderRadius(12).onClick(() => { this.search(item); }) }, (item: string) => item) }.width('100%')
        } else {
          Column({ space: 10 }) { Text('“' + this.query + '” 的结果(' + this.resultNames.length + ' 件)').fontSize(18).fontWeight(FontWeight.Bold).width('100%'); if (this.resultNames.length === 0) { Text('没有找到匹配商品,换个关键词试试。').fontSize(14).fontColor('#98A2B3').width('100%').padding(22).backgroundColor(Color.White).borderRadius(16) } else { ForEach(this.resultNames, (item: string) => { this.ProductRow(item) }, (item: string) => item) } }.width('100%')
        }
      }.width('100%').padding(18)
    }.width('100%').height('100%').backgroundColor('#F5F7FB')
  }
}

当前 ArkTS 页面代码

从样例到业务模块的迁移路径

当前页面适合用于说明 Search 的基本行为。迁移到业务模块时,建议不要直接把整个页面复制进项目,而是先分出四类内容:展示组件、输入模型、状态转换、外部能力调用。展示组件负责渲染已得到的状态;输入模型负责描述合法数据;状态转换负责处理用户动作和返回结果;外部能力调用负责网络、数据库、设备或系统接口。边界明确后,页面可以保持简洁,测试也能绕开真实环境完成大部分验证。

迁移过程中最容易被忽略的是失败路径。外部能力可能没有权限、网络可能超时、缓存可能过期、设备可能离线。页面需要决定在每一种情况下显示什么、保留什么、允许用户再次执行什么动作。将这些分支提前写清楚,可以避免线上问题出现时才临时加入难以验证的补丁。

关键操作后的页面状态

发布材料如何保持同步

这篇文章包含三个固定材料:默认运行图、操作后运行图和代码图。它们不是可以独立替换的配图。修改页面布局、状态名称或交互路径后,应重新运行页面,并重新生成与当前源码一致的图片;修改文章正文后,也要回查其中的控件名称、状态名和操作描述是否仍对应源码。当前页面的主题是 Search,正文不应借用其他主题的布局描述或交互结论。

发布到外部平台时,图片地址还需要能被平台抓取。本文使用的是公共 CDN 地址,复制 Markdown 后不应继续沿用旧的受限图片源。若某个发布平台对外链有额外限制,应在发布前用一篇文章做真实预览;预览失败时优先切换受支持的图片存储方式,而不是把失败截图当成文章内容的一部分。

先明确本篇回答的问题

Search 的 ArkUI 原生实践 讨论的不是孤立 API 的参数表,而是 Search 在当前 ArkUI 基础组件 页面里承担什么职责。读者打开页面后,首先能看到 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」。这些文字、控件和结果区域构成了最小可观察面:它们让人知道当前页面在处理什么输入、默认处于什么状态,以及下一步可以执行什么动作。后文的判断都回到这个可观察面,不把未出现在代码和运行图中的能力写成既有结论。

对于这类页面,常见误解是先从组件名字推导功能,再把页面当作验证材料。更稳妥的顺序是反过来:先记录首屏出现了什么,再查看状态变量和回调,最后讨论 Search 是否适合作为业务模块的一部分。这样写的原因很具体:同一个组件可以用于展示、编辑、导航或反馈;只有结合数据变化和边界条件,组件的职责才会稳定下来。

从首屏建立工作模型

初始运行图不是装饰材料。它至少需要回答三个问题:页面是否已经进入正确的应用界面;当前默认值是否能被读者理解;用户是否能找到主要操作入口。Search 的首屏通过 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 提供了这些线索。即使页面不依赖网络或系统服务,也应把默认状态写清楚,因为默认状态往往决定后续回调能够被怎样解释。

当前组件树包含 Row、Text、Column、Scroll、TextInput、Button。这组组件各自承担的职责需要分开看:承载信息的组件负责让状态可见,承载输入的组件负责收集意图,布局组件负责保证这些元素在有限空间内仍能被辨认。把三类职责混在一段描述里,后续排查时很难判断问题来自数据、事件还是布局。

状态归属:先分清谁拥有数据

源码中可以直接看到 State query、State submitted、State history、State resultNames。这些声明说明当前页面把会影响可见结果的数据放在组件内部处理。这个选择适合演示和单页交互:初始化路径短,回调可以直接修改状态,依赖该状态的 UI 会重新计算。它并不自动意味着状态只能留在页面中;当数据需要跨页面共享、需要恢复或需要接受系统回调时,状态来源可以替换为持久化存储、应用级状态或服务层返回值,但展示层仍应只读地消费一个明确结果。

这里应避免同时维护“业务值”和“显示值”两份可变副本。若两份数据都能被回调修改,页面在快速操作或异步返回时容易出现相互覆盖。更可控的做法是保留一个权威状态,派生文本、颜色、位置、进度或列表内容。当前页面中能直接看到的结果应由同一条状态链计算出来,这也是截图与代码能够互相核对的前提。

事件如何变成界面结果

页面同时包含 7 个点击入口和 1 个值变化入口。事件处理本身不应只被理解为“按钮能点”。一次回调至少包含四段关系:用户动作或系统通知作为输入;参数检查决定该输入能否进入业务分支;状态更新保存本次结果;依赖状态的组件重新绘制,并以文本、选择态、图形或提示的形式反馈。把这四段关系写清楚,可以帮助读者理解为什么同一个控件在不同页面里会出现不同效果。

在实际接入中,建议为每个关键事件留下一个可见结果。比如输入被接受时更新结果区,输入越界时保持原值并给出原因,能力调用失败时保留上一次可用数据并显示失败状态。只有日志而没有界面反馈,发布截图无法证明用户真正看到了什么;只有界面文字而没有可追溯的状态来源,后续问题也难以定位。

渲染与布局:结果要在真实设备尺寸下成立

Search 的页面不是只在设计尺寸上成立。文字长度变化、系统字体缩放、深色主题、刘海与圆角区域、横竖屏切换都会改变控件可用空间。当前实现使用的 Row、Text、Column、Scroll、TextInput、Button 需要在这些条件下保持信息层级:主信息不应被裁切,操作区不能和结果区互相覆盖,长内容应有滚动或截断策略。首屏截图只覆盖一种尺寸,因此正文需要把这些未出现在截图里的约束单独写出来。

布局调整时,先确定页面的稳定锚点,例如顶部标题、主要内容区和底部操作区;再决定可伸缩区域由谁占用剩余空间;最后才处理颜色、圆角和间距。这样排查某个元素位置异常时,可以先看父容器的约束,而不是在子组件上反复追加偏移量。对于 组件属性、尺寸、选中态与触控反馈,这种由外到内的检查顺序能减少偶发重叠问题。

数据输入与边界条件

演示页面常把输入写死,使正常路径看起来足够直接;业务页面却必须处理空值、重复触发、越界参数、能力未授权和异步返回顺序。Search 接入真实数据时,应先列出输入的来源与类型,再确定哪些值可直接展示,哪些值必须归一化,哪些值应该被拒绝。尤其是用户输入和跨设备数据,不能假设字段一定完整,也不能把错误对象直接拼进界面文本。

一条可执行的处理顺序是:读取输入后先做空值与范围检查;通过检查后更新权威状态;需要耗时操作时增加进行中标记;返回成功再提交结果;返回失败则恢复可操作状态并说明失败原因。这个顺序比“请求完成后直接改 UI”多出几步,但它能明确处理中、成功、失败三种状态,避免用户在等待期间重复提交。

资源释放的补充审查

围绕 Search,还需要把「资源释放」作为独立检查点。订阅、播放器、数据库连接和任务都应有退出页面或状态变化后的释放策略,避免后台继续持有页面引用。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

版本演进的补充审查

围绕 Search,还需要把「版本演进」作为独立检查点。新增字段或交互时应保持旧数据可读,并为缺少新字段的记录提供明确回退。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

验收标准的补充审查

围绕 Search,还需要把「验收标准」作为独立检查点。验收应描述能观察到的页面结果:出现什么文字、数值或选中态,而不只写“功能正常”。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

隐私处理的补充审查

围绕 Search,还需要把「隐私处理」作为独立检查点。页面、日志和截图都不应包含真实账号、令牌、地址或其他不适合发布的信息。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

弱网与离线的补充审查

围绕 Search,还需要把「弱网与离线」作为独立检查点。依赖外部数据的场景应区分加载中、缓存可用、请求失败和完全不可用,避免统一显示空白。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

维护成本的补充审查

围绕 Search,还需要把「维护成本」作为独立检查点。控制状态数量、回调入口和跨层依赖,能让后续修改更容易定位影响范围。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

初始化路径的补充审查

围绕 Search,还需要把「初始化路径」作为独立检查点。初始化应提供可解释的默认值。默认值不是占位符,它决定了用户第一次打开页面时如何理解后续操作。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

状态不变量的补充审查

围绕 Search,还需要把「状态不变量」作为独立检查点。应明确哪些字段可以修改、哪些字段只能由系统返回、哪些组合在任何时候都不应出现。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

重复触发的补充审查

围绕 Search,还需要把「重复触发」作为独立检查点。连续点击、快速切换和返回后再次进入都可能让回调以意外顺序执行,需要保证结果不会被旧值覆盖。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

错误呈现的补充审查

围绕 Search,还需要把「错误呈现」作为独立检查点。错误提示应说明用户能否重试、是否保留已有内容以及下一步应检查什么,而不是只显示技术异常。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

模块边界的补充审查

围绕 Search,还需要把「模块边界」作为独立检查点。将页面绘制与能力调用分开后,模拟器验证不依赖真实服务,真实服务也不会直接控制组件树。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

数据一致性的补充审查

围绕 Search,还需要把「数据一致性」作为独立检查点。当数据来自缓存和远端两条路径时,需要定义谁覆盖谁、何时刷新、失败时页面保留哪个版本。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

资源释放的补充审查

围绕 Search,还需要把「资源释放」作为独立检查点。订阅、播放器、数据库连接和任务都应有退出页面或状态变化后的释放策略,避免后台继续持有页面引用。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

版本演进的补充审查

围绕 Search,还需要把「版本演进」作为独立检查点。新增字段或交互时应保持旧数据可读,并为缺少新字段的记录提供明确回退。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

验收标准的补充审查

围绕 Search,还需要把「验收标准」作为独立检查点。验收应描述能观察到的页面结果:出现什么文字、数值或选中态,而不只写“功能正常”。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

隐私处理的补充审查

围绕 Search,还需要把「隐私处理」作为独立检查点。页面、日志和截图都不应包含真实账号、令牌、地址或其他不适合发布的信息。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

弱网与离线的补充审查

围绕 Search,还需要把「弱网与离线」作为独立检查点。依赖外部数据的场景应区分加载中、缓存可用、请求失败和完全不可用,避免统一显示空白。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

维护成本的补充审查

围绕 Search,还需要把「维护成本」作为独立检查点。控制状态数量、回调入口和跨层依赖,能让后续修改更容易定位影响范围。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

初始化路径的补充审查

围绕 Search,还需要把「初始化路径」作为独立检查点。初始化应提供可解释的默认值。默认值不是占位符,它决定了用户第一次打开页面时如何理解后续操作。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

状态不变量的补充审查

围绕 Search,还需要把「状态不变量」作为独立检查点。应明确哪些字段可以修改、哪些字段只能由系统返回、哪些组合在任何时候都不应出现。 当前源码中的 State query、State submitted、State history、State resultNames 与页面上的 「商品搜索」、「实时联想 · 历史记录 · 商品结果」、「006」、「搜索」、「搜索历史」、「清空」、「暂无历史记录」 共同构成了这次审查的起点。先记录触发前后的可见差异,再确认状态更新只发生在允许的分支中;若引入异步操作,还要比较请求开始、成功返回和失败返回三个时刻的界面。

这一检查点的目标不是增加抽象术语,而是提前写清楚修改后的判断条件:哪些输入应改变页面,哪些输入只能提示并保持原值,哪些输入需要等待外部结果。把条件、状态和可见结果写成同一组规则,后续无论替换组件、调整布局还是接入服务,都能回到同一套验收口径。

结语

Search 的 ArkUI 原生实践 的实现可以继续扩展,但每次扩展都应回到本文建立的证据链:先确认输入与状态归属,再确认事件如何更新状态,最后在真实页面中验证用户可见结果。对 Search 而言,这种顺序比单独罗列属性更容易发现边界条件,也便于让代码、截图和发布文章长期保持同步。

数学模型与形式化表达

在 Search 的实现中,一些关键概念可以用数学语言进行形式化描述,这有助于加深对技术原理的理解:

  • ArkUI 声明式渲染模型:  F ( State ) → UI \text{ArkUI 声明式渲染模型: } \mathcal{F}(\text{State}) \rightarrow \text{UI} ArkUI 声明式渲染模型F(State)UI
  • 组件树深度:  D = ∑ i = 1 n d i ( d i  为第  i  层子节点数 ) \text{组件树深度: } D = \sum_{i=1}^{n} d_i \quad (d_i \text{ 为第 } i \text{ 层子节点数}) 组件树深度D=i=1ndi(di 为第 i 层子节点数)
  • 状态更新复杂度:  O ( re-render ) = O ( dirty components ) \text{状态更新复杂度: } O(\text{re-render}) = O(\text{dirty components}) 状态更新复杂度O(re-render)=O(dirty components)
  • 表单校验逻辑:  ∀ f ∈ fields , valid ( f )    ⟹    submit allowed \text{表单校验逻辑: } \forall f \in \text{fields}, \text{valid}(f) \implies \text{submit allowed} 表单校验逻辑ffields,valid(f)submit allowed
  • 密码强度: strength ( p ) = { weak , ∣ p ∣ < 6 ok , 6 ≤ ∣ p ∣ < 12 strong , ∣ p ∣ ≥ 12 \text{密码强度: } \text{strength}(p) = \begin{cases} \text{weak}, & |p| < 6 \\ \text{ok}, & 6 \leq |p| < 12 \\ \text{strong}, & |p| \geq 12 \end{cases} 密码强度strength(p)= weak,ok,strong,p<66p<12p12
  • 输入长度约束:  L min ≤ ∣ input ∣ ≤ L max \text{输入长度约束: } L_{\text{min}} \leq |\text{input}| \leq L_{\text{max}} 输入长度约束LmininputLmax

这些数学表达不是抽象的装饰,而是对代码行为的精确刻画。理解它们,有助于开发者从更高的维度把握技术方案的本质。

架构与流程可视化

用图形化的方式表达 Search 的架构和流程,能够帮助读者快速建立整体认知:

2025-01-01 2025-01-03 2025-01-05 2025-01-07 2025-01-09 2025-01-11 2025-01-13 2025-01-15 2025-01-17 2025-01-19 2025-01-21 需求分析 API 设计 核心功能开发 单元测试编写 模拟器验证 问题修复 文章撰写 设计 实现 验证 Search 开发与验证周期
Created with Raphaël 2.3.0 开始 执行操作 校验通过? 显示成功 完成 显示错误 yes no

技术选型的底层逻辑:为什么选择 ArkTS 而非其他方案

在 Search 的实现过程中,一个绕不开的问题是:为什么选择 ArkTS 这条技术路线,而不是沿用传统的 Web 开发模式或引入跨端框架?这个问题的答案,恰恰是理解 HarmonyOS 原生应用开发精髓的关键。

从技术演进的视角来看,任何框架的兴起与衰落都遵循着相似的规律——它们都在试图解决特定时代下的特定矛盾。Web 技术解决了跨平台一致性的问题,但其代价是性能天花板受限于浏览器引擎;跨端框架试图在保持开发效率的同时弥合平台差异,但每一次系统版本升级都意味着适配成本的重新投入。ArkTS 的路线选择,本质上是在更高的维度上回答这个问题:当操作系统本身就是自己定义的时候,应用开发语言与系统能力的融合可以达到何种深度?

ArkUI 声明式 UI 框架的核心贡献,不在于它提供了多少组件,而在于它重新定义了"状态"与"界面"之间的关系。在传统的命令式开发中,开发者需要手动追踪哪些数据变化了、哪些视图需要更新,这种心智负担随着页面复杂度的增长呈指数级上升。而在声明式模型中,开发者只需要关注"状态是什么",框架自动推导出"界面应该变成什么样"。这种思维方式的转变, Δ UI = f ( Δ State ) \Delta \text{UI} = f(\Delta \text{State}) ΔUI=f(ΔState),正是 ArkTS 区别于传统开发范式的根本所在。

当然,任何技术选型都有其适用的边界。ArkTS 适合那些需要深度调用系统能力、追求极致性能、或者希望在鸿蒙生态中获得原生体验的场景。而对于纯内容的展示型页面,Web 技术仍然有其不可替代的灵活性。理解这些边界,比简单地站队某一种技术更为重要。

调试优先级矩阵

当 Search 出现异常时,建议按照以下优先级逐级排查,避免在非根因问题上浪费时间:

优先级 检查项 检查方法 典型问题
P0 状态值是否正确 在回调中打印状态值 状态未更新或更新错误
P1 事件是否绑定 确认 onClick/onChange 已绑定 组件可点但无响应
P2 条件分支是否进入 检查 if 条件表达式 分支逻辑错误
P3 组件是否被条件渲染排除 确认 if 标签包含组件 组件未渲染
P4 布局容器是否正确 检查父容器尺寸和权重 组件被挤出可视区

实现方案对比

方案 代码复杂度 可维护性 性能表现 适用场景
内联写法 简单页面、一次性组件
组件拆分 复杂页面、可复用组件
状态管理库 大规模应用、多页面共享状态
自定义 Builder 重复 UI 片段、模板化布局

关键 API 参数速查

参数 类型 必填 默认值 说明
width Length 自适应 组件宽度,支持百分比和 vp
height Length 自适应 组件高度,支持百分比和 vp
backgroundColor ResourceColor 透明 背景色设置
borderRadius Length 0 圆角半径
padding Padding 0 内边距
margin Margin 0 外边距
visibility Visibility Visible 可见性控制

核心设计原则

ArkUI 声明式设计
通过 @State 装饰器将数据与 UI 绑定,数据变化自动触发界面更新,无需手动操作 DOM。
组件以函数式方式组合,build() 方法描述 UI 结构,框架负责渲染差异。
状态驱动
所有界面变化都源于状态的改变,状态是 UI 的唯一数据来源。
状态变更通过装饰器(@State、@Prop、@Link 等)在组件树中传播。
组件化开发
页面由可复用的自定义组件组合而成,每个组件拥有独立的生命周期和状态。
组件之间通过装饰器定义数据传递关系,父组件向子组件传递数据,子组件通过事件向父组件通信。
平台原生能力
ArkTS 可以直接调用 HarmonyOS 系统 API,包括网络、存储、传感器、多媒体等能力。
不需要引入第三方框架即可实现跨设备协同、分布式数据管理等高级功能。

参考资料

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐