HarmonyOS7 自定义组件封装:从重复代码到优雅复用只需要3步
📅 2026/7/21 7:22:14
👁️ 阅读次数
📝 编程学习
文章目录
- 前言
- 重复代码的痛
- 3 步封装法
- 第一步:提取结构
- 第二步:定义接口
- 第三步:完善逻辑
- 使用封装好的 Card 组件
- 组件设计原则
- 写在最后
前言
上个月接手一个项目,打开代码一看——5 个页面,每个页面都有一段几乎一样的卡片 UI 代码,复制粘贴改改文字就完事。改需求的时候更惨,5 个地方都得改一遍,漏改一个就 bug。
重复代码是技术债的起点,而组件封装就是还债最快的方式。
这篇文章分享我总结的 3 步封装法,再通过一个完整的 Card 组件实战,带你从重复代码走到优雅复用。
重复代码的痛
重复代码不只是"看着不爽"的问题,它有三重罪:
- 改一处漏一处—— 5 个地方一样的代码,改需求只改了 4 个,第 5 个就是 bug
- 代码膨胀—— 本来 200 行能搞定的事,写了 800 行
- 不敢重构—— 重复代码太多,牵一发动全身,谁都不敢动
说白了,不封装 = 每次改动都是定时炸弹。
3 步封装法
第一步:提取结构
把重复的 UI 结构抽出来,做成独立的@Component。
封装前:每个页面直接写卡片结构
// 页面AColumn(){Image($r('app.media.pic1')).width(60).height(60).borderRadius(8)Text('标题A').fontSize(16).fontWeight(FontWeight.Bold)Text('描述A').fontSize(13).fontColor('#999')}.padding(16).backgroundColor('#fff').borderRadius(12)封装后:抽成一个组件,结构固定
@Componentstruct Card{build(){Column(){Image($r('app.media.pic1')).width(60).height(60).borderRadius(8)Text('标题A').fontSize(16).fontWeight(FontWeight.Bold)Text('描述A').fontSize(13).fontColor('#999')}.padding(16).backgroundColor('#fff').borderRadius(12)}}这一步只是"搬了个家",数据还是写死的。但已经有价值了——改结构只改一处。
第二步:定义接口
把写死的数据改成参数,让组件可配置。这就是组件的"接口"。
改造后:
@Componentstruct Card{@Propicon:Resource=$r('app.media.default')@Proptitle:string=''@Propdesc:string=''build(){Column(){Image(this.icon).width(60).height(60).borderRadius(8)Text(this.title).fontSize(16).fontWeight(FontWeight.Bold)Text(this.desc).fontSize(13).fontColor('#999')}.padding(16).backgroundColor('#fff').borderRadius(12)}}关键代码讲解:
@Prop icon: Resource—— 图片用Resource类型,兼容$r()和网络图片@Prop title: string = ''—— 默认值设为空串,避免不传参时显示 undefined- 用
@Prop而不是@State,因为数据来自父组件,组件内部不需要拥有
这一步之后,组件就活了——传不同的参数,展示不同的内容。
第三步:完善逻辑
加入事件回调、条件渲染、样式定制等能力,让组件真正通用。
最终版本:
@Componentstruct Card{@Propicon:Resource=$r('app.media.default')@Proptitle:string=''@Propdesc:string=''@PropshowArrow:boolean=trueonCardClick?:()=>voidbuild(){Row(){Image(this.icon).width(48).height(48).borderRadius(8).objectFit(ImageFit.Cover)Column(){Text(this.title).fontSize(16).fontWeight(FontWeight.Bold).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})Text(this.desc).fontSize(13).fontColor('#999').margin({top:4}).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})}.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({left:12})if(this.showArrow){Image($r('sys.media.ohos_ic_public_arrow_right')).width(16).height(16).fillColor('#ccc')}}.width('100%').padding(16).backgroundColor('#fff').borderRadius(12).onClick(()=>{if(this.onCardClick){this.onCardClick()}})}}逐行讲解关键逻辑:
layoutWeight(1)—— 中间文字区域占满剩余空间,图标和箭头固定大小maxLines(1)+textOverflow—— 标题超长时显示省略号,不会撑爆布局if (this.showArrow)—— 条件渲染,不需要箭头的场景传showArrow: falseonCardClick?: () => void—— 可选回调,点击卡片时通知父组件onClick里的空值检查—— 回调是可选的,不传就不触发,避免运行时崩溃
使用封装好的 Card 组件
对比一下,封装前 vs 封装后:
封装前(页面里重复写):
// 页面A、页面B、页面C... 每个都写一坨Row(){Image(...).width(48).height(48)Column(){Text(...);Text(...)}Image(箭头).width(16)}.padding(16).borderRadius(12).onClick(...)封装后(一行搞定):
Card({icon:$r('app.media.settings'),title:'设置',desc:'应用设置与偏好',onCardClick:()=>{this.routerTo('Settings')}})代码量从 10+ 行变成 1 行,改样式只改组件本身。这就是封装的价值。
组件设计原则
封装组件不是越多越好,记住这几条:
- 两次重复就可以考虑封装—— 别等到第五次,第二次就该动手
- 接口要少而精—— 一个组件超过 8 个参数就该拆了
- 提供合理默认值—— 让最常用的场景可以少传参
- 回调用可选属性——
onXxx?: () => void,不传不报错 - 样式也能参数化—— 不只是数据,圆角大小、内边距这些也可以暴露
- 别过度封装—— 只出现一次的 UI 不用抽组件,强行封装反而增加理解成本
写在最后
组件封装这件事,说白了就是用一次的时间投入,换以后每次的省心。
3 步封装法——提取结构、定义接口、完善逻辑——听起来简单,但很多人卡在第一步就不愿意动了。毕竟复制粘贴是最快的,封装要动脑子。
但相信我,下次需求变更的时候,你会感谢今天花 10 分钟封装的自己。
编程学习
技术分享
实战经验