HarmonyOS掌上记账APP开发实践第28篇:自定义组件封装策略 — 从通用 Header 到业务组件的分层设计

📅 2026/7/17 20:22:32 👁️ 阅读次数 📝 编程学习
HarmonyOS掌上记账APP开发实践第28篇:自定义组件封装策略 — 从通用 Header 到业务组件的分层设计

自定义组件封装策略 — 从通用 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 的组件分为三个层次:

App(应用根节点)

TabBar(底部导航)

BillPage(账单页)

StatisticsPage(统计页)

MinePage(我的页)

CommonHeader(通用标题栏)

DailyBillGroup(每日分组)

BillCard(账单卡片)

CategoryIcon(分类图标)

ChartSummary(图表摘要)

CalendarSummary(日历摘要)

MemberCard(会员卡片)

SettingItem(设置项)

经验法则

  1. 单一职责:一个组件只做一件事。BillCard只负责展示单条账单,不负责布局列表。
  2. 接口归一化:组件的@Param应设计为统一的 Model 类型,而非零散的基础类型。
  3. 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接收AssetGroupModelAssetItemModel数据对象,渲染出统一的卡片 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)}}

四、最佳实践总结

  1. 优先使用 @ComponentV2:新一代装饰器性能更好、语法更简洁。
  2. @Param 与 @Local 职责分明:外部数据用@Param,内部状态用@Local
  3. 单向数据流:父传子通过 props,子传父通过回调。
  4. 组件粒度适中:过细则文件碎片化,过粗则不可复用。
  5. 保持 props 不可变:子组件不修改传入的数据,确保数据流可预测。
  6. 接口归一化:推荐传入完整 Model 对象,而非零散字段。

五、参考文档

  • 自定义组件最佳实践
  • @ComponentV2 装饰器说明
  • @Param 装饰器