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)
  }
}
@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)
  }
}
@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实现跨层级通信

七、总结与行动建议

核心要点回顾

  1. 自定义组件基础:使用@Component装饰器创建可复用的UI单元,通过构造函数传递属性
  2. 组件通信机制:掌握@Prop、@Link、@Provide/@Consume、@ObjectLink等装饰器的使用场景
  3. 样式复用方案:使用@Builder、@Extend、@Styles实现UI和样式的复用
  4. 性能优化:通过@Reusable装饰器实现组件复用,提升长列表性能
  5. 生命周期管理:合理使用aboutToAppear、aboutToDisappear、aboutToReuse等生命周期方法

行动建议

  1. 重构现有代码:将重复的UI代码提取为自定义组件,统一组件接口规范
  2. 创建组件库:为项目建立基础组件库,包括按钮、卡片、弹窗等常用组件
  3. 性能优化实践:为列表项组件添加@Reusable装饰器,优化滑动性能
  4. 样式统一管理:使用@Extend创建全局样式扩展,确保UI风格一致性
  5. 组件文档化:为每个自定义组件编写使用文档,包括属性说明、使用示例

通过本篇文章的学习,你已经掌握了HarmonyOS自定义组件开发的核心能力。下一篇文章将深入讲解动画与交互动效,帮助你实现更流畅、更生动的用户界面。

posted @ 2025-12-23 23:17  J_____P  阅读(0)  评论(0)    收藏  举报