19 自定义组件设计与复用:从 UI 片段到独立组件

📅 2026/7/10 14:00:38
19 自定义组件设计与复用:从 UI 片段到独立组件
19 自定义组件设计与复用从 UI 片段到独立组件前言图19 自定义组件设计与复用从 UI 片段到独立组件 运行效果截图HarmonyOS NEXT在 ArkUI 开发中随着页面复杂度上升将重复的 UI 片段抽取为可复用组件是一件自然而然的事情。自定义组件不仅能消除重复代码还能实现统一维护、独立测试和跨页面复用。本文以鹿鹿·笔迹心理分析项目中的 4 个通用组件——HmTitleBar导航栏、HandwritingRadar雷达图、EmptyState空状态、PosterTemplate海报——为例深入解析自定义组件的设计原则、状态管理和接口规范。鸿蒙官方·自定义组件指南developer.huawei.com项目源码仓库harmony-app GitHub图鹿鹿项目的组件复用架构——HmTitleBar/HandwritingRadar/EmptyState 等通用组件被多个页面复用容器型页面组件HmTitleBar 标题栏HandwritingRadar 雷达图EmptyState 空状态MatchRing 圆环EmotionTrendChart 折线图DuetReportPageReportDetailPageArchivePageMetaServiceCard 元服务卡片HomePageArchivePageTrendPage一、自定义组件的基本结构1.1 组件声明与导出一个标准的 ArkUI 自定义组件由Component装饰器修饰的export struct构成Componentexportstruct HandwritingRadar{// ① 属性声明Prop / StatePropdata:RadarPoint[][]PropradarSize:number280// ② 私有成员privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(...)// ③ 生命周期aboutToAppear():void{...}// ④ 自定义方法privatedraw():void{...}// ⑤ 构建函数必须build(){Canvas(this.context){...}}}1.2 组件接口设计原则一个好的自定义组件接口应遵循以下原则原则说明HmTitleBar 示例最小接口只暴露必要属性内部实现细节私有暴露title/showBack/onAction不暴露内部动画状态默认值所有 Prop 必须有合理的默认值showBack: false默认不显示返回按钮单向数据流数据从父组件传入子组件不修改HmTitleBar 不修改title内容回调代替逻辑操作通过回调暴露不内嵌业务逻辑onAction?: () void由父组件实现单一职责一个组件只做一件事HmTitleBar 只负责标题栏展示不负责页面逻辑二、HmTitleBar可配置标题栏组件2.1 组件接口设计[HmTitleBar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HmTitleBar.ets) 是项目中复用最多的组件——被DuetReportPage、ReportDetailPage等多个页面引用Componentexportstruct HmTitleBar{// 可配置属性通过 Prop 暴露 Proptitle:string// 主标题Propsubtitle:string// 副标题PropshowBack:booleanfalse// 是否显示返回按钮PropactionIcon:Resource|nullnull// 右侧图标PropactionText:string// 右侧文字// 回调接口 onAction?:()void// 右侧操作回调onBack?:()void// 返回按钮回调可选// 内部状态外部不可见 StatesubtitleOpacity:number0}2.2 灵活的回调设计组件的返回按钮通过onBack回调实现灵活导航// 子组件中if(this.showBack){Text(←).fontSize(20).fontColor(AppColors.TEXT).onClick((){if(this.onBack){this.onBack()// ① 优先使用自定义回调}else{this.getUIContext().getRouter().back()// ② 默认返回上一页}})}设计亮点onBack为可选回调。如果父组件不传递组件自动使用router.back()执行默认的返回导航。这种有自定义用自定义无自定义用默认的模式大大提升了组件的易用性。2.3 组件复用在父组件中使用 HmTitleBar// DuetReportPage.ets — 首页 副标题 无返回HmTitleBar({title:合盘报告,subtitle:鹿鹿 × 小李 · 第 87 天,showBack:false})// ReportDetailPage.ets — 简化调用HmTitleBar({title:笔迹解读,showBack:true})2.4 组件接口类型汇总参数类型默认值是否必传说明titlestring推荐传主标题文字subtitlestring可选副标题文字showBackbooleanfalse可选是否显示 ← 按钮actionIconResourcenullnull可选actionTextstring可选操作文字onAction() voidundefined可选操作回调onBack() voidundefined可选返回回调提示所有Prop都有默认值HmTitleBar 甚至支持HmTitleBar({})空调用依然完整渲染——这在调试和原型阶段非常有用。三、HandwritingRadar数据展示组件3.1 数据接口定义[HandwritingRadar.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets) 将数据接口与组件分离定义// 数据接口与组件分开解耦合exportinterfaceRadarPoint{label:string// 维度名称如能量场value:number// 维度得分0-100}3.2 组件与数据的关系Componentexportstruct HandwritingRadar{Propdata:RadarPoint[][]// 数据由父组件传入PropradarSize:number280// 尺寸由父组件控制// 内部实现外界不可见privatecontext:CanvasRenderingContext2DStateWatch(onDrawProgressChange)drawProgress:number0build(){Canvas(this.context).width(this.radarSize).height(this.radarSize)}}数据驱动设计RadarPoint[]数组定义了 6 个维度的标签和分数父组件ReportDetailPage / MetaServiceCard负责从 DB 加载数据HandwritingRadar 只负责渲染不关心数据来源通过radarSize适应不同展示场景280vp 大尺寸 / 160vp 小尺寸// ReportDetailPage — 大尺寸HandwritingRadar({data:radarData,radarSize:240})// MetaServiceCard — 小尺寸元服务卡片HandwritingRadar({data:radarData,radarSize:160})四、组件设计模式4.1 展示型组件的设计展示型组件如 HandwritingRadar、HmTitleBar的特点是// 展示型组件数据由父组件传入子组件只负责渲染Componentexportstruct PureDisplayComponent{Propdata:SomeType// ① 数据从外部流入Propconfig:ConfigType// ② 配置从外部流入onEvent?:()void// ③ 事件向外通知// ④ 没有 State或者极少使用不管理可变状态build(){...}}优点完全可预测给定相同 props渲染相同 UI容易测试不需要模拟数据库高复用性不绑定业务逻辑4.2 容器型组件的设计容器型组件如 HomePage、ReportDetailPage则负责// 容器型组件管理状态 加载数据 组合子组件EntryComponentstruct ReportDetailPage{Stateresult:AnalysisResult|nullnull// ① 管理状态aboutToAppear(){this.loadReport()}// ② 加载数据build(){Scroll(){Column(){// ③ 组合展示型子组件HandwritingRadar({data:radarData})// ...}}}}4.3 展示型 vs 容器型对比维度展示型组件容器型组件状态管理极少或没有 State多 State管理数据流数据来源Prop 传入自身加载DB/API复用性高跨页面低页面专有测试难度低高需要 mock示例HmTitleBar / HandwritingRadarHomePage / ReportDetailPage五、组件导入与路径管理5.1 组件目录结构项目中的通用组件统一放在common/components/目录下common/components/ ├── EmotionTrendChart.ets # 情绪趋势折线图 ├── EmptyState.ets # 空状态组件 ├── HandwritingRadar.ets # 6 维雷达图 ├── HmTitleBar.ets # 大标题栏 ├── MatchRing.ets # 匹配度圆环 └── PosterTemplate.ets # 分享海报模板5.2 组件导入方式// 方式一命名导入推荐import{HmTitleBar}from../common/components/HmTitleBarimport{HandwritingRadar}from../common/components/HandwritingRadar// 方式二按需导入import{EmptyState}from../common/components/EmptyStateimport{MatchRing}from../common/components/MatchRing// 方式三类型导入importtype{RadarPoint}from../common/components/HandwritingRadarimporttype{PosterContent,PosterTemplateId}from../common/components/PosterTemplate六、组件引用计数6.1 各组件复用统计组件被引用页面引用次数HmTitleBarDuetReportPage、ReportDetailPage、…5HandwritingRadarReportDetailPage、MetaServiceCard、DuetReportPage3EmptyStateHomePage、ArchivePage2MatchRingDuetReportPage1PosterTemplateSharePage1EmotionTrendChartTrendPage16.2 组件的演进路径一个典型的组件从无到有的演进路径第一阶段内联代码 ↓ 发现重复 第二阶段抽取为 Builder组件内复用 ↓ 需要在其他页面使用 第三阶段拆分为独立 Component 文件 ↓ 需要更复杂的接口 第四阶段完善接口 文档 默认值指导原则不要过早抽象。当同一段 UI 在第 2 次出现时才考虑抽象为 Builder第 3 次出现时才拆分为独立组件。七、组件设计注意事项7.1 避免组件粒度过细// ❌ 粒度过细每个 Text 都做成组件TextItem({text:标题,size:16})TextItem({text:正文,size:14})TextItem({text:标签,size:12})// 这些调用比直接写 Text 更长没有带来实质好处// ✅ 合理粒度完整的功能模块HandwritingRadar({data:points})// 整个雷达图HmTitleBar({title:首页})// 整个标题栏EmptyState({ctaText:开始})// 整个空状态7.2 合理使用默认值// ✅ 好的默认值设计Componentexportstruct EmptyState{Proptitle:string还没有分析记录快来试试吧Propsubtitle:string上传一段手写笔记 · 让 AI 读懂你PropctaText:string拍下第一笔}// 调用方可以省略大部分参数EmptyState()// 使用全部默认值EmptyState({ctaText:试试手写})// 只覆盖部分参数7.3 组件文档组件的注释应该说明其用途和参数/** * 双人匹配度圆环 * 给 DuetReportPage 用 * * 单环渐变 中心匹配百分比 副标题 */Componentexportstruct MatchRing{PropmatchValue:number78// 匹配度 0-100PropringSize:number200// 圆环尺寸Proplabel:string匹配度// 中心标签}八、重构案例从内联到组件8.1 原始代码内联build(){Row(){if(this.showBack){Text(←).onClick(()router.back())}Column(){Text(标题).fontSize(28)Text(副标题).fontSize(12)}}}8.2 重构为组件Componentexportstruct HmTitleBar{Proptitle:stringPropsubtitle:stringPropshowBack:booleanfalseonBack?:()voidbuild(){Row(){if(this.showBack){Text(←).onClick((){this.onBack?this.onBack():this.getUIContext().getRouter().back()})}Column(){Text(this.title).fontSize(AppFonts.TITLE_L)if(this.subtitle){Text(this.subtitle).fontSize(AppFonts.CAPTION)}}}}}重构收益代码行数减少 40%从 5 个页面各写一次 → 1 个组件引用 5 次统一修改标题栏样式只需改一个文件新增页面使用标题栏时只需 1 行调用总结本文通过项目中 4 个真实自定义组件总结了 ArkUI 组件设计的核心原则和实践经验最小接口原则只暴露必要属性内部实现细节私有化合理默认值所有 Prop 提供合理的默认值降低使用门槛展示型 vs 容器型展示型组件纯渲染、高复用容器型组件管理状态和业务组件演进内联 → Builder → 独立 Component逐步抽象回调代替业务操作回调由父组件注入子组件不嵌入业务逻辑下一篇文章将深入EmptyState 空状态设计解析一个含入场动画、呼吸脉冲和定时器管理的完整状态组件。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力参考资源ArkUI 自定义组件开发指南Component 装饰器参考Prop 单向数据流组件复用设计模式[HmTitleBar 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HmTitleBar.ets)[HandwritingRadar 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)[EmptyState 组件源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmptyState.ets)[PosterTemplate 组件源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/PosterTemplate.ets)HarmonyOS 官方示例