HarmonyOS掌上记账APP开发实践第28篇:自定义组件封装策略 — 从通用 Header 到业务组件的分层设计
📅 2026/7/17 20:22:32
👁️ 阅读次数
📝 编程学习
自定义组件封装策略 — 从通用 Header 到业务组件的分层设计
一、引言
在大型应用中,合理的组件化设计是保证代码可维护性和复用性的基石。鸿蒙的 ArkTS 通过@ComponentV2装饰器支持自定义组件,允许开发者将页面拆分为可复用的独立模块。MoneyTrack 项目从通用基础组件(如 CommonHeader)到业务领域组件(如 AssetSummaryCard、AssetCard),构建了一套清晰的组件分层体系,实现了代码的高复用和低耦合。
二、核心知识点
2.1 @ComponentV2 与 @Component(V1) 对比
鸿蒙 ArkTS 提供了两代组件装饰器。@ComponentV2(推荐)相较于@Component(V1)在响应式和性能上有显著提升:
| 对比维度 | @ComponentV2 | @Component(V1) |
|---|---|---|
| 数据装饰器 | @Param / @Local / @Computed | @State / @Prop / @Link |
| 响应式粒度 | 属性级,精准触发更新 | 对象引用级,整体替换才触发 |
| 性能表现 | 仅更新变动的属性 | 整个组件重建 |
| 语法简洁度 | 不需要 .build() 内部赋初值 | 略繁琐 |
| 推荐程度 | 强烈推荐(新项目应使用) | 兼容旧项目 |
// @ComponentV2 写法(推荐)@ComponentV2exportstruct MyComponent{@Paramcount:number=0@LocalisVisible:boolean=truebuild(){/* ... */}}// @Component(V1) 写法(旧)@Componentexportstruct OldComponent{@Statecount:number=0@PropisVisible:boolean=truebuild(){/* ... */}}2.2 @Param 与 @Local 的角色分配
在@ComponentV2中,数据装饰器的选择决定了属性的职责:
@Param:接收父组件传入的外部属性(即 props),由父组件驱动更新。子组件不应修改@Param属性。@Local:组件内部管理的私有状态,仅供组件自身使用,外界不可直接赋值。
@ComponentV2exportstruct UserCard{@ParamuserName:string=''// 父组件传入,只读@ParamavatarUrl:string=''// 父组件传入,只读@LocalisExpanded:boolean=false// 组件内部状态@LocallikeCount:number=0// 组件内部状态build(){Column(){Text(this.userName).fontSize(16)if(this.isExpanded){Text('详细资料...')}}}}分配原则:凡是从父组件传入的数据用@Param,组件自己管理的交互状态用@Local。
2.3 组件通信方式
自定义组件之间通过单向数据流进行通信:
- 父→子:通过
@Param属性传递数据 - 子→父:通过回调函数(事件)通知父组件
// 父组件使用@ComponentV2exportstruct ParentPage{@Localitems:number[]=[1,2,3]build(){Column(){ForEach(this.items,(item:number)=>{ChildCard({value:item,onDelete:(id:number)=>{this.items=this.items.filter(v=>v!==id)}})})}}}// 子组件定义@ComponentV2exportstruct ChildCard{@Paramvalue:number=0@ParamonDelete:(id:number)=>void=()=>{}build(){Row(){Text(`${this.value}`)Button('删除').onClick(()=>this.onDelete(this.value))}}}这种模式确保了数据从父到子单向流动,子组件通过回调通知父组件执行修改,避免了双向绑定的混乱。
2.4 组件封装的最佳粒度
组件拆分需要遵循"高内聚、低耦合"原则。MoneyTrack 的组件分为三个层次:
经验法则:
- 单一职责:一个组件只做一件事。
BillCard只负责展示单条账单,不负责布局列表。 - 接口归一化:组件的
@Param应设计为统一的 Model 类型,而非零散的基础类型。 - props 不可变性:子组件应只读地消费
@Param,不直接修改父组件数据。
2.5 CommonHeader 完整实现
以下是 MoneyTrack 中 CommonHeader 的完整代码:
@ComponentV2exportstruct CommonHeader{@Paramtitle:string=''@ParamonBack:()=>void=()=>{}@ParamrightText:string=''@ParamonRightClick:()=>void=()=>{}@ParamisDarkMode:boolean=falsebuild(){Row(){// 左侧返回按钮Image($r('app.media.ic_back')).width(24).height(24).onClick(()=>this.onBack())// 中间标题Text(this.title).fontSize(18).fontWeight(FontWeight.Bold).fontColor(this.isDarkMode?'#FFFFFF':'#333333')Blank()// 右侧可选操作if(this.rightText.length>0){Text(this.rightText).fontSize(14).fontColor('#007AFF').onClick(()=>this.onRightClick())}}.padding({left:16,right:16,top:12,bottom:12}).width('100%').backgroundColor(this.isDarkMode?'#1A1A1A':'#FFFFFF')}}三、项目代码案例
3.1 CommonHeader 通用标题栏
在products/entry/src/main/ets/components/CommonHeader.ets中,CommonHeader 封装了统一的导航栏。该组件在 BillDetailPage、AssetDetailPage、SettingPage 等多个页面中复用,保证了导航栏的视觉统一。
3.2 AssetSummaryCard / AssetCard 业务卡片
在资产模块中,业务卡片组件封装了资产数据展示的公共逻辑:
- AssetSummaryCard:展示资产总览摘要(总资产、净资产等汇总指标)
- AssetCard:展示单个资产项目名称、金额、账期信息
这些组件通过@Param接收AssetGroupModel或AssetItemModel数据对象,渲染出统一的卡片 UI。
3.3 组件定义示例
@ComponentV2exportstruct AssetCard{@ParamassetData:AssetGroupModel=newAssetGroupModel()@ParamonTap:(id:number)=>void=()=>{}build(){Column(){Text(this.assetData.name).fontSize(16).fontWeight(FontWeight.Bold)Text(`¥${this.assetData.amount.toFixed(2)}`).fontSize(24)}.gesture(TapGesture().onAction(()=>{this.onTap(this.assetData.id)})).padding(16).backgroundColor(Color.White).borderRadius(12)}}四、最佳实践总结
- 优先使用 @ComponentV2:新一代装饰器性能更好、语法更简洁。
- @Param 与 @Local 职责分明:外部数据用
@Param,内部状态用@Local。 - 单向数据流:父传子通过 props,子传父通过回调。
- 组件粒度适中:过细则文件碎片化,过粗则不可复用。
- 保持 props 不可变:子组件不修改传入的数据,确保数据流可预测。
- 接口归一化:推荐传入完整 Model 对象,而非零散字段。
五、参考文档
- 自定义组件最佳实践
- @ComponentV2 装饰器说明
- @Param 装饰器
编程学习
技术分享
实战经验