鸿蒙 自定义组件封装(带Props/Events的通用卡片组件)
1. 引言
在鸿蒙(HarmonyOS)应用开发中,UI 组件的复用性直接影响开发效率与代码可维护性。随着应用功能的复杂化,开发者经常需要重复实现具有相似结构但细节不同的 UI 块(如商品卡片、用户信息卡片、设置项卡片),若每次都从头编写代码,不仅会导致冗余,还会增加后期维护成本。
鸿蒙的 ArkUI 框架 提供了强大的自定义组件能力,允许开发者将通用的 UI 结构封装为 带 Props(属性)和 Events(事件)的组件,实现“一次封装,多处复用”。本文将以 通用卡片组件 为例,深入讲解如何通过 Props 传递动态数据(如标题、图片、描述),通过 Events 实现交互回调(如点击事件),并封装一个可配置、可扩展的通用组件,适用于商品展示、用户信息、设置项等多种场景。
2. 技术背景
2.1 鸿蒙 ArkUI 的组件化思想
ArkUI 是鸿蒙生态的原生 UI 开发框架,基于 声明式范式(类似 React/Vue),通过组件树构建界面。其核心设计理念包括:
- 组件化:将 UI 拆分为独立的、可复用的组件,每个组件封装自身的结构、样式与逻辑;
- Props 传参:父组件通过 属性(Props) 向子组件传递数据(如文本、图片 URL、配置参数);
- Events 通信:子组件通过 事件(Events) 向父组件反馈用户交互(如点击、滑动),实现双向通信;
- 状态管理:通过
@State
、@Prop
等装饰器管理组件内部/外部状态,支持响应式更新。
2.2 通用卡片组件的需求背景
在移动应用中,“卡片”是最常见的 UI 模式之一——它通常包含 标题、副标题、图片、描述文本、操作按钮 等元素,用于展示结构化信息(如商品、用户、通知)。不同场景下的卡片虽然结构相似,但细节差异大(如电商卡片需要价格和购买按钮,用户卡片需要头像和昵称),因此需要通过 Props 动态配置内容,通过 Events 处理交互,避免重复编写相似代码。
3. 应用使用场景
3.1 场景1:电商商品卡片
- 需求:展示商品图片、名称、价格,支持点击卡片跳转详情页或添加购物车;
3.2 场景2:用户信息卡片
- 需求:显示用户头像、昵称、简介,支持点击头像查看个人资料;
3.3 场景3:设置项卡片
- 需求:呈现设置项标题、描述(如“开启夜间模式”),支持开关切换或点击进入子页面;
3.4 场景4:新闻资讯卡片
- 需求:展示新闻标题、摘要、发布时间,支持点击跳转原文;
4. 不同场景下的详细代码实现
4.1 环境准备
- 开发工具:华为 DevEco Studio(集成 ArkUI 框架);
- 核心概念:
- 自定义组件:通过
@Component
装饰器定义,封装 UI 结构与逻辑; - Props:通过
@Prop
或直接参数传递数据(如标题、图片 URL),父组件控制子组件内容; - Events:通过回调函数(如
onClick?: () => void
)向父组件传递交互事件; - 响应式设计:使用
@State
管理组件内部状态(如选中状态),@Prop
接收外部状态;
- 自定义组件:通过
- 注意事项:
- 自定义组件的样式(如卡片圆角、间距)需通过
@Styles
或内联样式定义,支持灵活配置; - 事件的命名需清晰(如
onCardClick
、onButtonClick
),避免与系统事件冲突。
- 自定义组件的样式(如卡片圆角、间距)需通过
4.2 典型场景:通用卡片组件封装(带 Props/Events)
4.2.1 代码实现(ArkTS)
// 通用卡片组件(GenericCard.ets)
@Component
export struct GenericCard {
// Props:通过父组件传递的配置参数(支持可选/必选)
@Prop title: string = '默认标题'; // 卡片标题(必选,默认值)
@Prop subtitle?: string; // 副标题(可选)
@Prop imageUrl?: string; // 图片 URL(可选)
@Prop description?: string; // 描述文本(可选)
@Prop showButton?: boolean = false; // 是否显示操作按钮(可选,默认不显示)
@Prop buttonText?: string = '操作'; // 按钮文本(可选,默认“操作”)
@Prop onCardClick?: () => void; // 卡片点击事件(可选)
@Prop onButtonClick?: () => void; // 按钮点击事件(可选)
// 组件内部状态(示例:按钮选中状态,非必需)
@State isButtonPressed: boolean = false;
build() {
// 卡片容器:使用 Column 垂直布局
Column() {
// 图片区域(如果提供了 imageUrl)
if (this.imageUrl) {
Image(this.imageUrl)
.width('100%')
.height(120)
.objectFit(ImageFit.Cover)
.borderRadius(8)
.margin({ bottom: 12 })
}
// 文本内容区域
Column() {
// 标题(必选)
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 4 })
// 副标题(可选)
if (this.subtitle) {
Text(this.subtitle)
.fontSize(14)
.fontColor('#666666')
.margin({ bottom: 8 })
}
// 描述文本(可选)
if (this.description) {
Text(this.description)
.fontSize(14)
.fontColor('#999999')
.lineHeight(20)
.margin({ bottom: 12 })
}
}
.alignItems(HorizontalAlign.Start) // 文本左对齐
.layoutWeight(1) // 占满剩余空间
// 操作按钮(如果启用)
if (this.showButton) {
Button(this.buttonText || '操作')
.width('100%')
.height(40)
.fontSize(16)
.backgroundColor(this.isButtonPressed ? '#E0E0E0' : '#007AFF') // 按压状态变色
.fontColor(Color.White)
.borderRadius(6)
.margin({ top: 12 })
.onClick(() => {
this.isButtonPressed = !this.isButtonPressed; // 模拟按压效果
this.onButtonClick?.(); // 触发按钮点击事件(父组件传入的回调)
})
}
}
.width('100%') // 卡片宽度占满父容器
.padding(16) // 内边距
.backgroundColor(Color.White) // 背景色
.borderRadius(12) // 圆角
.shadow({ // 阴影效果(提升层次感)
radius: 4,
color: '#00000010',
offsetX: 0,
offsetY: 2
})
.onClick(() => {
this.onCardClick?.(); // 触发卡片点击事件(父组件传入的回调)
})
}
}
4.2.2 原理解释
- Props 传参:父组件通过
@Prop
装饰的属性(如title
、imageUrl
、onCardClick
)向子组件传递数据与交互逻辑; - 动态渲染:通过条件判断(如
if (this.imageUrl)
)控制图片、副标题、描述文本、按钮的显示/隐藏,适配不同场景需求; - 事件回调:子组件通过
this.onCardClick?.()
和this.onButtonClick?.()
触发父组件传入的回调函数,实现交互反馈(如跳转页面、提交表单); - 样式配置:卡片的圆角、阴影、内边距等样式通过链式调用(如
.borderRadius(12)
)定义,支持灵活调整; - 状态管理:按钮的按压状态(
isButtonPressed
)通过@State
管理,实现视觉反馈(如颜色变化)。
4.3 典型场景1:电商商品卡片(使用通用组件)
4.3.1 代码实现(父组件调用)
// 商品列表页面(ProductList.ets)
import { GenericCard } from './GenericCard';
@Entry
@Component
struct ProductList {
build() {
Column() {
// 商品卡片1:带图片、标题、价格、购买按钮
GenericCard({
title: '高端智能手机',
subtitle: '6GB+128GB 全网通',
imageUrl: 'https://example.com/phone.jpg',
description: '骁龙8 Gen2处理器,拍照旗舰',
showButton: true,
buttonText: '加入购物车',
onCardClick: () => {
console.log('点击了商品卡片,跳转详情页');
// 实际项目中可跳转到商品详情页(如 router.pushUrl)
},
onButtonClick: () => {
console.log('点击了购买按钮,添加到购物车');
// 实际项目中可调用购物车逻辑
}
})
// 商品卡片2:仅标题和描述(无图片和按钮)
GenericCard({
title: '限时优惠活动',
description: '全场商品满299减50,活动截止明日',
onCardClick: () => {
console.log('点击了活动卡片,跳转活动页');
}
})
}
.width('100%')
.padding(20)
}
}
4.3.2 原理解释
- 灵活配置:父组件通过传递不同的 Props(如
imageUrl
、showButton
)控制卡片的显示内容,无需修改通用组件代码; - 交互解耦:点击卡片或按钮的逻辑由父组件通过
onCardClick
和onButtonClick
回调实现(如跳转页面、调用 API),符合单一职责原则; - 复用性:同一通用组件同时用于商品展示和活动推广,减少重复代码。
4.4 典型场景2:用户信息卡片(使用通用组件)
4.4.1 代码实现(父组件调用)
// 用户资料页面(UserProfile.ets)
import { GenericCard } from './GenericCard';
@Entry
@Component
struct UserProfile {
build() {
Column() {
// 用户信息卡片:带头像(通过 imageUrl)、昵称(title)、简介(description)
GenericCard({
title: '张三',
subtitle: '高级开发者',
imageUrl: 'https://example.com/avatar.jpg',
description: '专注于鸿蒙原生应用开发,热爱技术分享',
showButton: true,
buttonText: '查看详情',
onCardClick: () => {
console.log('点击了用户卡片,跳转个人主页');
},
onButtonClick: () => {
console.log('点击了详情按钮,打开用户详情页');
}
})
}
.width('100%')
.padding(20)
}
}
4.4.2 原理解释
- 场景适配:通过传递
imageUrl
(头像)、subtitle
(职业)、description
(简介)等 Props,将通用组件适配为用户信息展示; - 交互扩展:按钮文本(
buttonText
)自定义,点击事件(onButtonClick
)可关联到用户详情页跳转。
5. 原理解释
5.1 自定义组件的核心机制
鸿蒙 ArkUI 的自定义组件通过以下步骤实现:
- 组件定义:使用
@Component
装饰器标记一个结构体(如GenericCard
),内部通过build()
方法定义 UI 结构; - Props 传参:父组件通过
@Prop
装饰的属性(如title
、imageUrl
)向子组件传递数据,子组件通过this.属性名
访问; - Events 通信:子组件通过回调函数(如
onCardClick?: () => void
)接收父组件的交互逻辑,内部触发时调用this.回调函数?.()
; - 样式与状态:通过链式调用(如
.width('100%')
)定义样式,通过@State
管理组件内部动态状态(如按钮按压效果)。
5.2 核心特性总结
特性 | 说明 | 典型应用场景 |
---|---|---|
Props 传参 | 父组件动态配置子组件的内容(如标题、图片、是否显示按钮) | 多场景复用(商品/用户/设置卡片) |
Events 回调 | 子组件向父组件反馈交互事件(如点击卡片、点击按钮) | 交互逻辑解耦(跳转页面、提交表单) |
条件渲染 | 通过 if (this.属性) 控制子元素的显示/隐藏,适配不同配置 |
灵活布局(有图/无图、有按钮/无按钮) |
样式配置 | 支持圆角、阴影、内边距等样式链式调用,提升视觉效果 | 品牌一致性(统一的卡片设计) |
状态管理 | 通过 @State 管理组件内部状态(如按钮按压),实现微交互 |
增强用户体验(视觉反馈) |
6. 原理流程图及原理解释
6.1 通用卡片组件工作流程图
graph LR
A[父组件调用 GenericCard] --> B[传递 Props(title/imageUrl/onClick...)]
B --> C[GenericCard 组件接收 Props]
C --> D[根据 Props 动态渲染 UI(图片/文本/按钮)]
D --> E[监听用户交互(点击卡片/按钮)]
E --> F[触发 Events 回调(onCardClick/onButtonClick)]
F --> G[父组件执行对应逻辑(跳转页面/调用 API)]
6.2 原理解释
- 数据流:父组件通过 Props 向子组件传递配置数据(如标题、图片 URL),子组件根据这些数据决定渲染哪些 UI 元素;
- 事件流:用户点击卡片或按钮时,子组件通过回调函数通知父组件,父组件执行具体的业务逻辑(如页面跳转、数据提交);
- 解耦设计:子组件仅负责 UI 渲染与事件触发,不包含具体业务逻辑(如“加入购物车”的具体实现),符合高内聚低耦合原则。
7. 环境准备
7.1 开发与测试环境
- 操作系统:Windows/macOS/Linux(开发机) + 鸿蒙设备(如华为手机/平板,用于真机测试);
- 开发工具:华为 DevEco Studio(集成 ArkUI 框架与组件调试工具);
- 关键配置:
- 项目模板:选择“Empty Ability”模板(支持 ArkUI 组件开发);
- 组件目录:将通用组件(如
GenericCard.ets
)放在src/main/ets/components/
目录下,便于复用; - 权限要求:无特殊权限(仅 UI 渲染,不涉及硬件/网络)。
- 测试设备:建议使用不同分辨率的鸿蒙设备(如手机竖屏/横屏、平板)测试组件的适配性。
7.2 兼容性检测代码
// 检测当前环境是否支持 ArkUI 组件(示例:验证 @Component 装饰器)
@Component
struct CompatibilityTest {
build() {
Text('ArkUI 组件功能正常')
.fontSize(16)
}
}
验证步骤:运行页面,观察是否正常显示文本(确认 ArkUI 基础功能可用)。
8. 实际详细应用代码示例(综合案例:电商详情页 + 推荐卡片)
8.1 场景描述
开发一个鸿蒙版电商应用的商品详情页,包含:
- 商品主卡片:展示商品主图、名称、价格,支持点击跳转详情;
- 推荐商品列表:底部展示多个推荐商品卡片(使用通用组件),每个卡片支持“加入购物车”操作。
8.2 代码实现(ArkTS)
// 电商详情页(ProductDetail.ets)
import { GenericCard } from './components/GenericCard';
@Entry
@Component
struct ProductDetail {
// 模拟推荐商品数据
private recommendedProducts: Array<{
id: number;
title: string;
subtitle: string;
imageUrl: string;
price: string;
}> = [
{ id: 1, title: '无线蓝牙耳机', subtitle: '降噪版', imageUrl: 'https://example.com/earphone.jpg', price: '¥299' },
{ id: 2, title: '智能手表', subtitle: '运动版', imageUrl: 'https://example.com/watch.jpg', price: '¥1299' },
{ id: 3, title: '充电宝', subtitle: '20000mAh', imageUrl: 'https://example.com/powerbank.jpg', price: '¥99' }
];
build() {
Scroll() {
// 商品主卡片(使用通用组件,配置为详情页样式)
GenericCard({
title: '高端智能手机',
subtitle: '6GB+128GB 全网通',
imageUrl: 'https://example.com/phone-main.jpg',
description: '骁龙8 Gen2处理器,1亿像素主摄,5000mAh长续航',
showButton: true,
buttonText: '查看详情',
onCardClick: () => {
console.log('点击主卡片,跳转商品详情页');
// 实际项目中调用 router.pushUrl('/detail/123')
},
onButtonClick: () => {
console.log('点击详情按钮,打开详情页');
}
})
// 推荐商品列表(循环渲染通用组件)
Text('为您推荐')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ top: 30, bottom: 15 })
ForEach(this.recommendedProducts, (product: {
id: number;
title: string;
subtitle: string;
imageUrl: string;
price: string;
}) => {
GenericCard({
title: product.title,
subtitle: product.subtitle,
imageUrl: product.imageUrl,
description: `售价:${product.price}`,
showButton: true,
buttonText: '加入购物车',
onCardClick: () => {
console.log(`点击推荐商品 ${product.id},跳转商品页`);
},
onButtonClick: () => {
console.log(`点击推荐商品 ${product.id} 的购物车按钮`);
}
})
.margin({ bottom: 12 }) // 卡片间距
})
}
.width('100%')
.height('100%')
.padding(20)
}
}
9. 运行结果
9.1 通用卡片基础功能
- 父组件通过传递不同的 Props(如
imageUrl
、showButton
),动态控制子组件的显示内容(有图/无图、有按钮/无按钮); - 点击卡片或按钮时,控制台输出对应的交互日志(如“点击了商品卡片”),父组件可扩展为实际业务逻辑。
9.2 电商详情页集成
- 商品主卡片展示高清主图与详细描述,点击后跳转详情页;
- 推荐商品列表通过循环渲染通用组件,每个卡片独立配置标题、图片、价格,点击“加入购物车”触发对应逻辑。
10. 测试步骤及详细代码
10.1 基础功能测试
- Props 传参验证:修改父组件传递的
title
、imageUrl
,观察子组件是否实时更新; - 事件回调测试:点击卡片或按钮,检查控制台是否输出正确的交互日志;
- 条件渲染测试:隐藏
imageUrl
或showButton
,确认对应 UI 元素不显示。
10.2 边界测试
- 空数据测试:不传递
imageUrl
或description
,验证组件是否正常渲染(仅显示标题和副标题); - 多语言测试:将
title
和description
改为非中文(如英文),确认文本显示无异常。
11. 部署场景
11.1 电商应用
- 适用场景:商品列表页、推荐商品页、用户个人中心(如订单卡片、优惠券卡片);
- 要求:通过 Props 动态配置不同类型卡片的内容,通过 Events 实现跳转、加购等业务逻辑。
11.2 社交应用
- 适用场景:用户动态卡片(如朋友圈)、群聊消息卡片、设置项卡片;
- 要求:支持图片、文本、按钮的灵活组合,适配不同交互需求(如点赞、评论)。
12. 疑难解答
12.1 问题1:子组件未接收到 Props 数据
- 可能原因:父组件传递的 Props 名称与子组件定义的
@Prop
属性名不一致(如父组件传title
,子组件定义@Prop cardTitle
); - 解决方案:确保父子组件的 Props 名称一致,或通过文档明确约定属性名。
12.2 问题2:事件回调未触发
- 可能原因:父组件未向子组件传递回调函数(如
onCardClick
未定义),或子组件调用时使用了错误的函数名; - 解决方案:检查父组件是否传递了所有需要的事件回调(如
onCardClick: () => { ... }
),子组件调用时使用this.onCardClick?.()
。
12.3 问题3:图片无法加载
- 可能原因:传递的
imageUrl
无效(如 URL 拼写错误、网络不可访问),或未处理图片加载失败的默认状态; - 解决方案:检查图片 URL 的有效性,或在子组件中添加
Image
的onError
回调(显示默认占位图)。
13. 未来展望
13.1 技术趋势
- 更强大的 Props 类型:未来可能支持复杂对象(如嵌套数据结构)作为 Props,进一步提升组件的配置灵活性;
- 内置动画支持:通用组件可能集成鸿蒙的动画 API(如
animateTo
),允许通过 Props 控制动画效果(如卡片展开/收起); - 跨页面复用:通过全局组件注册(如
globalThis
)实现跨页面的通用卡片复用,减少重复导入。
13.2 挑战
- 性能优化:当循环渲染大量通用组件(如 100+ 推荐商品)时,需关注内存占用与渲染效率(可通过虚拟列表优化);
- 多主题适配:不同应用主题(如暗色模式/亮色模式)下,通用组件的样式(如文字颜色、背景色)需动态适配;
- 无障碍支持:确保通用组件支持屏幕阅读器(如为图片添加
alt
文本,为按钮添加aria-label
)。
14. 总结
鸿蒙 ArkUI 的自定义组件封装(带 Props/Events)是提升 UI 开发效率与代码复用性的核心手段。通过 通用卡片组件 的实践,我们验证了:
- Props 传参:允许父组件动态配置子组件的内容(如标题、图片、交互按钮),适配多场景需求;
- Events 通信:实现子组件与父组件的交互解耦(如点击反馈、数据提交),符合单一职责原则;
- 灵活扩展:通过条件渲染与状态管理,通用组件可轻松扩展为电商卡片、用户卡片、设置项卡片等多种形态。
掌握自定义组件的开发技巧,不仅是鸿蒙开发的必备技能,更是构建高质量、可维护应用的基石。未来,随着 ArkUI 功能的持续增强(如更强大的动画、主题系统),通用组件的应用场景将更加广泛。
- 点赞
- 收藏
- 关注作者
评论(0)