Harmony学习之自定义组件开发
Harmony学习之自定义组件开发
一、场景引入
小明在开发电商应用时发现,商品卡片、用户头像、按钮等UI元素在多个页面重复出现,每次都要复制粘贴相同的代码。这不仅导致代码冗余,还增加了维护成本——修改一个样式需要在多个地方同步更新。更糟糕的是,由于缺乏统一的组件规范,不同页面的相同功能组件样式不统一,用户体验大打折扣。本篇文章将系统讲解HarmonyOS自定义组件的开发方法,帮助小明构建可复用、易维护的组件库。
二、核心概念
1. 自定义组件的作用
自定义组件是HarmonyOS应用开发的核心能力,通过@Component装饰器将UI结构封装为独立的可复用单元。与系统组件相比,自定义组件具有以下优势:
- 代码复用:一次开发,多处使用,减少重复代码
- 统一规范:确保UI风格和交互逻辑的一致性
- 维护便捷:修改一处,所有使用该组件的地方自动更新
- 性能优化:通过组件复用机制减少内存占用和渲染开销
2. 组件生命周期
自定义组件拥有完整的生命周期管理,理解这些生命周期方法对于合理管理资源和优化性能至关重要:
| 生命周期方法 | 触发时机 | 使用场景 |
|---|---|---|
| aboutToAppear | 组件实例创建后,build()执行前 | 数据初始化、网络请求 |
| aboutToDisappear | 组件销毁前 | 资源释放、取消订阅 |
| aboutToReuse | 组件复用时(@Reusable) | 数据更新、状态重置 |
| aboutToRecycle | 组件进入缓存池前 | 清理临时数据 |
页面级组件(@Entry装饰)还额外支持onPageShow、onPageHide、onBackPress等页面生命周期方法。
三、关键实现
1. 基础自定义组件
// src/main/ets/components/ProductCard.ets
@Component
export struct ProductCard {
// 组件属性,通过构造函数传递
private product: Product = new Product();
private onItemClick?: (product: Product) => void;
// 构造函数,支持属性传递
constructor(params?: { product: Product, onItemClick?: (product: Product) => void }) {
if (params) {
this.product = params.product;
this.onItemClick = params.onItemClick;
}
}
aboutToAppear(): void {
// 组件即将显示,可进行数据初始化
console.log('ProductCard aboutToAppear');
}
aboutToDisappear(): void {
// 组件即将销毁,清理资源
console.log('ProductCard aboutToDisappear');
}
build() {
Column({ space: 10 }) {
// 商品图片
Image(this.product.image)
.width(120)
.height(120)
.objectFit(ImageFit.Cover)
.borderRadius(8)
// 商品标题
Text(this.product.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 商品价格
Text(`¥${this.product.price}`)
.fontSize(16)
.fontColor(Color.Red)
// 销量信息
Text(`已售${this.product.sales}`)
.fontSize(12)
.fontColor(Color.Gray)
}
.width(140)
.padding(10)
.backgroundColor(Color.White)
.borderRadius(12)
.onClick(() => {
this.onItemClick?.(this.product);
})
}
}
2. 使用@Builder构建UI片段
@Builder装饰器用于封装可复用的UI片段,比自定义组件更轻量,适合简单的UI复用场景。
// 全局Builder函数
@Builder
function PriceTag(price: number, discount?: number) {
Row({ space: 5 }) {
if (discount) {
Text(`¥${discount}`)
.fontSize(16)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
Text(`¥${price}`)
.fontSize(12)
.fontColor(Color.Gray)
.decoration({ type: TextDecorationType.LineThrough })
} else {
Text(`¥${price}`)
.fontSize(16)
.fontColor(Color.Red)
}
}
}
// 局部Builder函数(在组件内部定义)
@Component
struct ProductCard {
@Builder
buildRating(rating: number) {
Row({ space: 2 }) {
ForEach(Array.from({ length: 5 }), (_, index) => {
Image(index < rating ? $r('app.media.star_filled') : $r('app.media.star_empty'))
.width(12)
.height(12)
})
}
}
build() {
Column() {
// ...其他UI
this.buildRating(this.product.rating)
}
}
}
3. 使用@Extend扩展组件样式
@Extend装饰器用于为系统组件创建可复用的样式扩展,支持参数传递和状态变量。
// 扩展Text组件样式
@Extend(Text)
function textPrimary(size: number = 16, color: Color = Color.Black) {
.fontSize(size)
.fontColor(color)
.fontWeight(FontWeight.Bold)
}
// 扩展Button组件样式
@Extend(Button)
function primaryButton() {
.backgroundColor('#FF3A3A')
.fontColor(Color.White)
.fontSize(16)
.height(44)
.width('100%')
.borderRadius(8)
}
// 使用示例
Text('商品标题').textPrimary(18, Color.Red)
Button('立即购买').primaryButton()
4. 组件复用优化(@Reusable)
对于频繁创建销毁的组件(如列表项),使用@Reusable装饰器可以显著提升性能。
@Reusable
@Component
struct ReusableProductCard {
@State product: Product = new Product();
// 组件复用时调用,更新数据
aboutToReuse(params: Record<string, any>): void {
this.product = params.product as Product;
}
build() {
ProductCard({ product: this.product })
.reuseId(this.product.id) // 设置唯一复用标识
}
}
// 在列表中使用
List() {
LazyForEach(this.dataSource, (item: Product) => {
ListItem() {
ReusableProductCard({ product: item })
}
})
}
四、组件通信
1. 父传子单向通信(@Prop)
@Component
struct ParentComponent {
@State title: string = '默认标题';
build() {
Column() {
ChildComponent({ title: this.title })
Button('修改标题')
.onClick(() => {
this.title = '新标题';
})
}
}
}
@Component
struct ChildComponent {
@Prop title: string; // 单向接收父组件数据
build() {
Text(this.title)
}
}
2. 父子双向通信(@Link)
@Component
struct ParentComponent {
@State count: number = 0;
build() {
Column() {
Text(`父组件: ${this.count}`)
ChildComponent({ count: $count }) // 使用$符号传递双向绑定
}
}
}
@Component
struct ChildComponent {
@Link count: number; // 双向绑定
build() {
Button('子组件+1')
.onClick(() => {
this.count++;
})
}
}
3. 跨层级通信(@Provide/@Consume)
@Entry
@Component
struct ParentComponent {
@Provide('themeColor') @State themeColor: Color = Color.Blue;
build() {
Column() {
MiddleComponent()
}
}
}
@Component
struct MiddleComponent {
build() {
Column() {
ChildComponent()
}
}
}
@Component
struct ChildComponent {
@Consume('themeColor') themeColor: Color;
build() {
Text('子组件')
.fontColor(this.themeColor)
}
}
4. 对象类型数据同步(@ObjectLink)
@Observed
class User {
name: string = '';
age: number = 0;
}
@Component
struct ParentComponent {
@State user: User = { name: '张三', age: 20 };
build() {
Column() {
ChildComponent({ user: this.user })
Button('修改年龄')
.onClick(() => {
this.user.age++;
})
}
}
}
@Component
struct ChildComponent {
@ObjectLink user: User;
build() {
Text(`姓名: ${this.user.name}, 年龄: ${this.user.age}`)
}
}
五、实战案例:商品卡片组件
1. 完整组件实现
// src/main/ets/components/ProductCard.ets
import { Product } from '../model/Product';
@Component
export struct ProductCard {
private product: Product = new Product();
private onItemClick?: (product: Product) => void;
constructor(params?: { product: Product, onItemClick?: (product: Product) => void }) {
if (params) {
this.product = params.product;
this.onItemClick = params.onItemClick;
}
}
build() {
Column({ space: 10 }) {
// 商品图片
Image(this.product.image)
.width(120)
.height(120)
.objectFit(ImageFit.Cover)
.borderRadius(8)
// 商品信息
Column({ space: 5 }) {
// 商品标题
Text(this.product.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 价格区域
this.buildPriceArea()
// 评分和销量
Row({ space: 10 }) {
this.buildRating()
Text(`已售${this.product.sales}`)
.fontSize(12)
.fontColor(Color.Gray)
}
}
}
.width(140)
.padding(10)
.backgroundColor(Color.White)
.borderRadius(12)
.onClick(() => {
this.onItemClick?.(this.product);
})
}
@Builder
private buildPriceArea() {
if (this.product.discountPrice) {
// 有折扣价
Row({ space: 5 }) {
Text(`¥${this.product.discountPrice}`)
.fontSize(16)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
Text(`¥${this.product.price}`)
.fontSize(12)
.fontColor(Color.Gray)
.decoration({ type: TextDecorationType.LineThrough })
}
} else {
// 原价
Text(`¥${this.product.price}`)
.fontSize(16)
.fontColor(Color.Red)
}
}
@Builder
private buildRating() {
Row({ space: 2 }) {
ForEach(Array.from({ length: 5 }), (_, index) => {
Image(index < this.product.rating ? $r('app.media.star_filled') : $r('app.media.star_empty'))
.width(12)
.height(12)
})
Text(this.product.rating.toFixed(1))
.fontSize(12)
.fontColor(Color.Gray)
}
}
}
2. 在页面中使用
// src/main/ets/pages/ProductList.ets
import { ProductCard } from '../components/ProductCard';
import { Product } from '../model/Product';
@Entry
@Component
struct ProductList {
@State products: Product[] = [];
aboutToAppear() {
this.loadProducts();
}
build() {
Column() {
// 商品列表
List() {
ForEach(this.products, (item: Product) => {
ListItem() {
ProductCard({
product: item,
onItemClick: this.onProductClick
})
}
})
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
private async loadProducts() {
// 模拟网络请求
this.products = [
{
id: '1',
title: '华为Mate 60 Pro',
price: 6999,
discountPrice: 5999,
image: $r('app.media.product1'),
rating: 4.8,
sales: 10000
},
{
id: '2',
title: 'iPhone 15 Pro Max',
price: 8999,
image: $r('app.media.product2'),
rating: 4.9,
sales: 20000
}
];
}
private onProductClick = (product: Product) => {
// 跳转到商品详情页
router.pushUrl({
url: 'pages/ProductDetail',
params: { productId: product.id }
});
}
}
六、最佳实践
1. 组件设计原则
单一职责原则:每个组件只负责一个特定功能,避免过度复杂化。如果一个组件超过200行代码,考虑拆分为多个子组件。
接口设计规范:
- 属性命名使用驼峰命名法,明确表达意图
- 为可选属性提供合理的默认值
- 使用TypeScript类型约束确保类型安全
- 事件回调使用on前缀,如onItemClick
性能优化策略:
- 对于列表项等频繁创建销毁的组件,使用@Reusable装饰器
- 避免在build方法中创建新对象或函数
- 合理使用@StorageLink进行状态持久化
- 复杂计算使用@Watch监听器优化重渲染
2. 组件复用场景
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 简单UI片段 | @Builder | 轻量级,适合简单的UI复用 |
| 复杂UI组件 | @Component | 完整的组件生命周期管理 |
| 样式复用 | @Extend | 扩展系统组件样式 |
| 列表项复用 | @Reusable + @Component | 长列表性能优化 |
| 跨层级通信 | @Provide/@Consume | 避免逐层传递props |
3. 常见问题与解决方案
问题1:组件复用时数据不同步
- 原因:没有正确实现aboutToReuse方法
- 解决方案:在aboutToReuse中更新组件状态
问题2:组件样式不统一
- 原因:缺少统一的样式规范
- 解决方案:使用@Extend创建全局样式扩展函数
问题3:列表滑动卡顿
- 原因:组件频繁创建销毁
- 解决方案:使用@Reusable装饰器实现组件复用
问题4:组件通信复杂
- 原因:多层嵌套导致props传递繁琐
- 解决方案:使用@Provide/@Consume实现跨层级通信
七、总结与行动建议
核心要点回顾
- 自定义组件基础:使用@Component装饰器创建可复用的UI单元,通过构造函数传递属性
- 组件通信机制:掌握@Prop、@Link、@Provide/@Consume、@ObjectLink等装饰器的使用场景
- 样式复用方案:使用@Builder、@Extend、@Styles实现UI和样式的复用
- 性能优化:通过@Reusable装饰器实现组件复用,提升长列表性能
- 生命周期管理:合理使用aboutToAppear、aboutToDisappear、aboutToReuse等生命周期方法
行动建议
- 重构现有代码:将重复的UI代码提取为自定义组件,统一组件接口规范
- 创建组件库:为项目建立基础组件库,包括按钮、卡片、弹窗等常用组件
- 性能优化实践:为列表项组件添加@Reusable装饰器,优化滑动性能
- 样式统一管理:使用@Extend创建全局样式扩展,确保UI风格一致性
- 组件文档化:为每个自定义组件编写使用文档,包括属性说明、使用示例
通过本篇文章的学习,你已经掌握了HarmonyOS自定义组件开发的核心能力。下一篇文章将深入讲解动画与交互动效,帮助你实现更流畅、更生动的用户界面。

浙公网安备 33010602011771号