
鸿蒙 ArkUI 组件基础复盘从两个 UI 卡片回到 ComponentV2、状态管理和组件分层一、为什么做完功能后要回头复习基础最近做了两个比较典型的 ArkUI UI 卡片1. AI 总结卡片展示 AI 标识、总结文案和查看详情入口。 2. AI 问题走马灯卡片展示上下两排推荐问题支持自动轮播、手动滑动暂停、点击问题进入 AI 聊天。这两个组件看起来只是 UI但实际涉及ComponentV2 struct build() Builder Local Param Event ObservedV2 Trace Callback Swiper TouchEvent ViewModel Controller 数组响应式更新一开始写功能时目标往往是“先跑通”。但做完以后回头看会发现这些基础概念决定了代码能不能维护、能不能复用、后续需求变更时会不会崩。本文所有代码均为通用示例不包含公司项目路径、业务常量、接口地址或内部封装。二、自定义组件ComponentV2、struct、build()一个 ArkUI 自定义组件常见结构如下ComponentV2exportstruct AiSummaryCard{build(){Column(){Text(AI 总结)Text(这里展示总结内容)}}}三者关系可以这样理解ComponentV2 声明这是一个使用 V2 状态管理能力的 ArkUI 自定义组件。 struct 组件结构体。ArkUI 自定义组件一般通过 struct 进行声明。 build() 组件的 UI 描述入口。页面最终展示什么主要在 build() 中声明。一句话记忆ComponentV2 是身份标识struct 是组件载体build() 是 UI 出口。build()适合写 UI 结构、简单条件渲染、样式声明不适合写接口请求、路由跳转、复杂数组处理、定时器创建等副作用逻辑。复杂逻辑应该放到 Controller、ViewModel、生命周期函数或事件回调中。三、为什么要把卡片抽成独立组件如果把 AI 总结卡片、走马灯卡片、搜索框、列表内容全部写在一个大组件里父组件会越来越臃肿。更好的方式是AiSummaryCard({summaryText:this.viewModel.summaryText,onClickDetail:(){this.controller.openAiDetail()}})AiQuestionMarqueeCard({topQuestionList:this.viewModel.topQuestionList,bottomQuestionList:this.viewModel.bottomQuestionList,isPaused:this.viewModel.paused,onQuestionClick:(question:string){this.controller.openAiChat(question)}})组件抽离的意义1. 降低父组件复杂度。 2. 提高复用性。 3. 统一样式。 4. 方便单独调试。 5. 父组件只关心传什么数据不关心组件内部怎么画。 6. 更符合 Component ViewModel Controller 的分层思路。四、BuilderUI 片段函数Builder可以理解为“UI 片段函数”。BuilderAskButtonBuilder(){Row(){Text(问AI)}.width(88).height(40).borderRadius(12)}它适合拆分一个按钮 一个问题气泡 一个头部区域 一个列表 item 一个轮播区域但Builder不是业务函数。它可以有简单 UI 判断但不应该承载接口请求、路由跳转、复杂数据处理。可以这样记Builder 是 UI 片段函数不是业务函数。五、Local组件内部响应式状态Local适合保存当前组件内部使用、并且变化后需要刷新 UI 的状态LocalprivatecurrentIndex:number0Localprivateexpanded:booleanfalseLocalprivatekeyword:string它的特点1. 只属于当前组件。 2. 父组件不需要传。 3. 状态变化后当前组件 UI 需要刷新。不是所有成员变量都要加Local。比如普通常量、timerId、控制器对象通常用普通private成员即可。Local 当前组件自己的响应式状态 private 当前组件自己的普通变量 Param 父组件传进来的外部状态六、Param父组件向子组件传数据Param用来接收父组件传入的数据ComponentV2exportstruct AiSummaryCard{ParamsummaryText:stringbuild(){Text(this.summaryText)}}父组件使用AiSummaryCard({summaryText:this.viewModel.summaryText})走马灯组件中也可以这样写ParamtopQuestionList:string[][]ParambottomQuestionList:string[][]ParamisPaused:booleanfalse这表示父组件负责准备数据。 子组件负责根据数据展示 UI。一般不建议子组件直接修改Param接收到的数据。更推荐子组件通过Event通知父组件由父组件或 Controller 修改 ViewModel 后再传回来。七、Event子组件向父组件抛事件Event本质上是父组件传给子组件的回调函数。EventonQuestionClick?:Callbackstring子组件点击问题时.onClick((){this.onQuestionClick?.(question)})父组件使用AiQuestionMarqueeCard({onQuestionClick:(question:string){this.controller.openAiChat(question)}})这个过程就是子组件发生点击 ↓ 子组件把 question 抛给父组件 ↓ 父组件拿到 question ↓ 父组件决定跳转、请求、埋点等业务行为这样做可以避免子组件和具体业务强绑定。八、为什么子组件不直接跳转页面如果在子组件里直接写HMUtil.push({pageUrl:AgentChatPage})这个组件就被写死了只能跳这个页面 只能用这个路由工具 别的页面想复用就很困难更推荐EventonAskClick?:Callbackvoid.onClick((){this.onAskClick?.()})父组件决定具体行为AiQuestionMarqueeCard({onAskClick:(){this.controller.openAiChat()}})这样组件可以被不同页面复用。九、ObservedV2 和 Trace在 V2 状态管理中ViewModel 常见写法如下ObservedV2exportclassAiQuestionViewModel{TracetopQuestionList:string[][]TracebottomQuestionList:string[][]Tracepaused:booleanfalse}可以这样理解ObservedV2 修饰一个可以被状态管理系统观测的类。 Trace 修饰类中需要被追踪的属性。ViewModel 适合使用ObservedV2因为 ViewModel 本来就是页面状态集合例如 loading、list、keyword、paused、summaryText 等字段。这些字段变化后UI 往往需要跟着刷新。ObservedV2 修饰 ViewModel 类 Trace 修饰 ViewModel 中需要驱动 UI 刷新的字段十、为什么数组更新推荐重新赋值不推荐this.viewModel.list.push(newItem)更推荐this.viewModel.listthis.viewModel.list.concat([newItem])或者this.viewModel.list[...this.viewModel.list,newItem]原因是push 修改的是原数组内容数组引用地址没有变化。 重新赋值一个新数组字段引用变化更明显响应式系统更容易识别并刷新 UI。所以在 ViewModel 中更新数组时尽量使用新数组赋值。十一、结合走马灯需求为什么拆成上下两个数组假设原始数据是 10 条[A, B, C, D, E, F, G, H, I, J]如果每次都在组件内部切上排前 5 个 下排后 5 个当你把第一个元素移到最后[B, C, D, E, F, G, H, I, J, A]再重新切分就变成上排B C D E F 下排G H I J A这样原本下排的内容可能跑到上排视觉上会跳动。更好的方式是 Controller 先拆成两个数组topQuestionList: A B C D E bottomQuestionList: F G H I JCard 只负责接收这两个数组并渲染不关心原始数据怎么来。十二、展示逻辑和业务逻辑怎么区分Card 组件里的展示逻辑包括1. 用 Row / Column / Stack 怎么布局。 2. 上下两个 Swiper 怎么摆。 3. 问题气泡怎么画。 4. 问AI按钮怎么画。 5. 字体、颜色、圆角、阴影。 6. 根据 isPaused 控制 autoPlay。业务逻辑包括1. 请求推荐问题数据。 2. 校验接口返回数组。 3. 拆分上下两排。 4. 跳转 AI 聊天页。 5. 携带 question 参数。 6. 埋点。 7. 登录判断。 8. 异常处理。一个好维护的组件应该尽量做到组件只负责展示和事件抛出。 业务逻辑放到 Controller。 状态放到 ViewModel。十三、手动滑动暂停状态放哪里手动滑动暂停 1 秒可以这样设计ViewModel 保存 paused 状态。 Controller 控制什么时候 paused true。 控制 1 秒后 paused false。 Card 接收 isPaused。 根据 isPaused 控制 Swiper 是否 autoPlay。示例publicpauseMarqueeStart():void{this.viewModel.pausedtruethis.clearResumeTimer()}publicpauseMarqueeEnd():void{this.clearResumeTimer()this.resumeTimerIdsetTimeout((){this.viewModel.pausedfalse},1000)}Card 里只负责抛事件privatehandleTouchEvent(event:TouchEvent):void{if(event.typeTouchType.Down){this.onTouchStart?.()return}if(event.typeTouchType.Up||event.typeTouchType.Cancel){this.onTouchEnd?.()}}这样如果以后产品说暂停 2 秒只改 Controller不改 Card。十四、这轮基础复习的收获1. ComponentV2 声明组件struct 是组件载体build 是 UI 出口。 2. build 里应该主要写声明式 UI不适合堆复杂业务。 3. Builder 用来拆 UI 片段不是业务函数。 4. Local 是组件内部响应式状态。 5. Param 是父组件传给子组件的数据。 6. Event 是子组件抛给父组件的事件本质是回调函数。 7. ObservedV2 适合修饰 ViewModel 这类可观测类。 8. Trace 修饰 ViewModel 中需要驱动 UI 刷新的字段。 9. 数组更新尽量重新赋值新数组避免 push 后 UI 不刷新。 10. 可复用组件不应该写死请求、跳转和具体业务。十五、总结这次不是单纯做一个走马灯卡片而是通过真实功能把 ArkUI 组件开发的基础重新串了一遍。从实现角度看我学到了Swiper、Scroll、Scroller、Marquee 的适用场景。 Swiper 通过 interval、duration、curve 模拟近似走马灯。 TouchEvent 可以处理用户按下、滑动、抬起。从架构角度看我学到了Controller 负责业务逻辑。 ViewModel 负责响应式状态。 Card 组件负责展示。 Param 负责父传子。 Event 负责子传父。对鸿蒙开发实习来说这类复盘很有价值。因为真正写业务时不只是把 UI 画出来还要考虑组件职责、状态流向、复用性和后续维护成本。参考链接自定义组件https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-create-custom-components状态管理 V2 总览https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-state-management-overviewMVVM 模式 V2https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-mvvm-v2Local 装饰器https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-new-localParam 装饰器https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-paramEvent 装饰器https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-eventObservedV2 和 Tracehttps://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-new-observedv2-and-trace自定义组件生命周期https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-page-custom-components-lifecycleSwiper 组件https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper触摸事件https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-events-touch