前置知识: HarmonyOS

自定义组件

2 minIntermediate2026/6/14

自定义组件与生命周期

概述

自定义组件是 HarmonyOS 应用构建的基础单元。通过 @Component 装饰器定义组件,使用 @Prop/@Link 实现组件间通信,配合生命周期回调管理资源。良好的组件设计是构建可维护应用的关键。

基础概念

组件生命周期

回调说明触发时机
aboutToAppear组件即将出现构建自定义组件时
aboutToDisappear组件即将销毁组件被移除时
onPageShow页面显示页面切入前台
onPageHide页面隐藏页面切到后台
onBackPress返回键按下用户按返回键

组件通信方式

方式装饰器数据流向适用场景
属性传递@Prop父 -> 子只读数据展示
双向绑定@Link父 <-> 子子组件需修改父数据
事件回调函数属性子 -> 父通知父组件
跨层级@Provide祖先 -> 后代深层嵌套通信

快速上手

基本自定义组件

// 定义可复用的按钮组件
@Component
export struct MyButton {
  @Prop text: string = '按钮'
  @Prop bgColor: string = '#007DFF'
  onButtonClick?: () => void  // 事件回调

  build() {
    Button(this.text)
      .width(200)
      .height(50)
      .backgroundColor(this.bgColor)
      .fontColor(Color.White)
      .borderRadius(25)
      .onClick(() => this.onButtonClick?.())
  }
}

// 使用自定义组件
@Entry
@Component
struct ButtonPage {
  @State count: number = 0

  build() {
    Column() {
      Text(`计数: ${this.count}`).fontSize(30)
      MyButton({
        text: '增加',
        onButtonClick: () => this.count++
      })
      MyButton({
        text: '减少',
        bgColor: '#FF4444',
        onButtonClick: () => this.count--
      })
    }
  }
}

带生命周期的组件

@Component
struct Timer {
  @Prop duration: number = 60
  @State remaining: number = 60
  private intervalId: number = -1

  // 组件即将出现:启动定时器
  aboutToAppear() {
    this.remaining = this.duration
    this.intervalId = setInterval(() => {
      if (this.remaining > 0) {
        this.remaining--
      } else {
        clearInterval(this.intervalId)
      }
    }, 1000)
  }

  // 组件即将销毁:清理定时器
  aboutToDisappear() {
    if (this.intervalId !== -1) {
      clearInterval(this.intervalId)
    }
  }

  build() {
    Text(`${this.remaining}s`)
      .fontSize(40)
      .fontColor(this.remaining <= 10 ? Color.Red : Color.Black)
  }
}

详细用法

卡片组件

// 通用的卡片组件
@Component
struct Card {
  @Prop title: string = ''
  @Prop subtitle: string = ''
  @Prop showAction: boolean = false
  onAction?: () => void
  @BuilderParam content: () => void  // 插槽:自定义内容

  build() {
    Column() {
      // 卡片头部
      Row() {
        Column() {
          Text(this.title).fontSize(18).fontWeight(FontWeight.Bold)
          if (this.subtitle) {
            Text(this.subtitle).fontSize(14).fontColor('#999').margin({ top: 4 })
          }
        }.layoutWeight(1)

        if (this.showAction) {
          Text('更多').fontColor('#007DFF').onClick(() => this.onAction?.())
        }
      }.width('100%').padding({ left: 16, right: 16, top: 16 })

      // 卡片内容(插槽)
      Column() {
        this.content()
      }.padding(16)

    }
    .width('100%')
    .borderRadius(12)
    .backgroundColor(Color.White)
    .shadow({ radius: 5, color: '#1F000000', offsetY: 2 })
  }
}

// 使用
@Entry
@Component
struct CardPage {
  build() {
    Column() {
      Card({ title: '用户信息', subtitle: '基本信息' }) {
        Text('这是卡片内容区域')
      }

      Card({ title: '订单列表', showAction: true, onAction: () => {
        // 查看更多
      }}) {
        Text('订单1')
        Text('订单2')
      }
    }.padding(16)
  }
}

列表项组件

interface ContactItem {
  id: number
  name: string
  phone: string
  avatar: string
}

@Component
struct ContactRow {
  @Prop item: ContactItem = { id: 0, name: '', phone: '', avatar: '' }
  onItemClick?: (id: number) => void
  onItemLongPress?: (id: number) => void

  build() {
    Row() {
      Image(this.item.avatar)
        .width(50).height(50).borderRadius(25)

      Column() {
        Text(this.item.name).fontSize(16).fontWeight(FontWeight.Medium)
        Text(this.item.phone).fontSize(14).fontColor('#999').margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12 })
    }
    .width('100%')
    .height(70)
    .padding({ left: 16, right: 16 })
    .onClick(() => this.onItemClick?.(this.item.id))
    .gesture(LongPressGesture().onAction(() => this.onItemLongPress?.(this.item.id)))
  }
}

常见场景

加载状态组件

@Component
struct LoadingView {
  @Prop isLoading: boolean = false
  @Prop isEmpty: boolean = false
  @Prop errorMsg: string = ''
  @BuilderParam content: () => void

  build() {
    if (this.isLoading) {
      Column() {
        LoadingProgress().width(50).height(50).color('#007DFF')
        Text('加载中...').fontSize(14).fontColor('#999').margin({ top: 10 })
      }.width('100%').height('100%').justifyContent(FlexAlign.Center)
    } else if (this.errorMsg) {
      Column() {
        Text(this.errorMsg).fontSize(16).fontColor('#999')
        Button('重试').margin({ top: 20 })
      }.width('100%').height('100%').justifyContent(FlexAlign.Center)
    } else if (this.isEmpty) {
      Column() {
        Text('暂无数据').fontSize(16).fontColor('#999')
      }.width('100%').height('100%').justifyContent(FlexAlign.Center)
    } else {
      this.content()
    }
  }
}

注意事项

  • @Component 修饰的 struct 不支持继承,组件复用应使用组合和 @BuilderParam
  • aboutToAppear 中可以访问 @State 变量,适合做初始化
  • aboutToDisappear 中必须清理定时器、监听器等资源,避免内存泄漏
  • @BuilderParam 只能有一个,用于定义组件的内容插槽
  • 组件的属性传递是只读的,子组件不能直接修改父组件传入的非 @Link 属性
  • 事件回调使用可选链调用(?.()),避免回调未定义时报错

进阶用法

泛型组件模拟

// ArkTS 不支持泛型组件,但可以通过接口约束实现类似效果
interface ListDataSource {
  id: string
  title: string
  subtitle?: string
}

@Component
struct GenericList {
  @Prop items: ListDataSource[] = []
  @BuilderParam itemBuilder: (item: ListDataSource) => void

  build() {
    List() {
      ForEach(this.items, (item: ListDataSource) => {
        ListItem() {
          this.itemBuilder(item)
        }
      }, (item: ListDataSource) => item.id)
    }
  }
}