4、基础组件封装(一):Text组件的二次封装

Text 组件,说白了就是鸿蒙里最基础的文本显示单元。很多新手觉得它太简单,直接拿来用就行。但我在实际项目中踩过不少坑——比如字体不统一、颜色散落在各个页面、点击事件处理得乱七八糟。嗯,今天我们就来把它好好封装一下。

4.1 为什么要二次封装?

你想想看,一个项目里可能有几十个页面。如果每个页面都直接写 Text('你好'),那后期改字体、改颜色的时候,你就得满世界找。我个人习惯是把基础组件包一层,统一管理样式和行为。

举个例子,我之前做过一个电商项目,运营突然说要换品牌字体。如果没封装,你得改几百个 Text 组件。封装了?改一个地方就行。

核心目标:

  • 统一管理字体和颜色
  • 支持点击事件回调
  • 支持富文本样式(部分文字加粗、变色)

4.2 基础封装结构

我们先搭一个最基础的架子。我习惯用 @Component 装饰器,把 Text 包一层。

@Component
struct CustomText {
  @Prop text: string = ''
  @Prop fontSize: number = 16
  @Prop fontWeight: FontWeight = FontWeight.Normal
  @Prop fontColor: Color = Color.Black
  @Prop onClick: () => void = () => {}

  build() {
    Text(this.text)
      .fontSize(this.fontSize)
      .fontWeight(this.fontWeight)
      .fontColor(this.fontColor)
      .onClick(() => {
        this.onClick()
      })
  }
}

这里有几个关键点:

  • @Prop:让外部可以传参进来
  • 默认值:给每个属性一个合理的默认值,避免调用方漏传
  • onClick 回调:用箭头函数包装一层,保证 this 指向正确

小技巧:我个人习惯把 onClick 的默认值设为空函数 () => {},这样即使调用方没传,也不会报错。

4.3 支持自定义字体与颜色

字体这块,鸿蒙支持两种方式:系统字体和自定义字体。我建议把字体名称也做成可配置的。

@Component
struct CustomText {
  @Prop text: string = ''
  @Prop fontSize: number = 16
  @Prop fontWeight: FontWeight = FontWeight.Normal
  @Prop fontColor: Color = Color.Black
  @Prop fontFamily: string = 'HarmonyOS Sans'
  @Prop onClick: () => void = () => {}

  build() {
    Text(this.text)
      .fontSize(this.fontSize)
      .fontWeight(this.fontWeight)
      .fontColor(this.fontColor)
      .fontFamily(this.fontFamily)
      .onClick(() => {
        this.onClick()
      })
  }
}

颜色方面,我建议用 Color 枚举或者十六进制字符串。我个人更倾向于十六进制,因为设计稿给的都是 #FF0000 这种格式。

避坑指南:我曾经在项目里直接用字符串传颜色,结果发现鸿蒙的 Color 类型和字符串不兼容。后来统一用 Color.parse('#FF0000') 转换,才搞定。

4.4 点击事件回调的优雅处理

点击事件看起来简单,但有几个细节要注意:

  1. 防重复点击:用户手抖点两下,回调可能触发两次
  2. 点击态反馈:文字颜色变暗或者背景闪一下,让用户知道点到了

我一般这样处理:

@State private var isClicked: boolean = false
private var clickTimer: number = 0

build() {
  Text(this.text)
    .fontSize(this.fontSize)
    .fontColor(this.isClicked ? Color.Gray : this.fontColor)
    .onClick(() => {
      if (this.clickTimer > 0) {
        return  // 防重复点击
      }
      this.isClicked = true
      this.clickTimer = setTimeout(() => {
        this.isClicked = false
        this.clickTimer = 0
      }, 200)
      this.onClick()
    })
}

这里用了一个 200 毫秒的防抖。为什么是 200?嗯,我测试过,这个时间既能防止误触,又不会让用户觉得卡顿。

4.5 富文本样式支持

富文本是 Text 组件的一个痛点。鸿蒙原生 Text 不支持部分文字加粗或变色。我封装了一个简单的方案:用 Span 组件。

@Component
struct RichText {
  @Prop segments: RichTextSegment[] = []

  build() {
    Text() {
      ForEach(this.segments, (item: RichTextSegment) => {
        Span(item.text)
          .fontSize(item.fontSize)
          .fontWeight(item.bold ? FontWeight.Bold : FontWeight.Normal)
          .fontColor(item.color)
      })
    }
  }
}

interface RichTextSegment {
  text: string
  fontSize?: number
  bold?: boolean
  color?: Color
}

调用方式也很直观:

RichText({
  segments: [
    { text: '这是普通文字', fontSize: 16 },
    { text: '这是加粗红色', fontSize: 16, bold: true, color: Color.Red },
    { text: '这是蓝色小字', fontSize: 12, color: Color.Blue }
  ]
})

注意:Span 组件必须放在 Text 容器内,不能单独使用。这个坑我当初找了半天文档才搞明白。

4.6 完整封装示例

把上面所有功能整合到一起,就是一个完整的 CustomText 组件:

@Component
struct CustomText {
  @Prop text: string = ''
  @Prop fontSize: number = 16
  @Prop fontWeight: FontWeight = FontWeight.Normal
  @Prop fontColor: Color = Color.Black
  @Prop fontFamily: string = 'HarmonyOS Sans'
  @Prop richSegments?: RichTextSegment[]
  @Prop onClick: () => void = () => {}

  @State private var isClicked: boolean = false
  private var clickTimer: number = 0

  build() {
    if (this.richSegments && this.richSegments.length > 0) {
      // 富文本模式
      Text() {
        ForEach(this.richSegments, (item: RichTextSegment) => {
          Span(item.text)
            .fontSize(item.fontSize ?? this.fontSize)
            .fontWeight(item.bold ? FontWeight.Bold : this.fontWeight)
            .fontColor(item.color ?? this.fontColor)
        })
      }
      .fontFamily(this.fontFamily)
      .onClick(() => this.handleClick())
    } else {
      // 普通文本模式
      Text(this.text)
        .fontSize(this.fontSize)
        .fontWeight(this.fontWeight)
        .fontColor(this.isClicked ? Color.Gray : this.fontColor)
        .fontFamily(this.fontFamily)
        .onClick(() => this.handleClick())
    }
  }

  private handleClick() {
    if (this.clickTimer > 0) return
    this.isClicked = true
    this.clickTimer = setTimeout(() => {
      this.isClicked = false
      this.clickTimer = 0
    }, 200)
    this.onClick()
  }
}

4.7 使用场景总结

场景 推荐方式 说明
普通文本 CustomText({ text: '你好' }) 使用默认字体颜色
带点击事件 CustomText({ text: '点我', onClick: () => {} }) 自动防抖
富文本 CustomText({ richSegments: [...] }) 支持混合样式
自定义字体 CustomText({ text: '标题', fontFamily: 'MyFont' }) 需提前注册字体

最后说一句:封装不是越复杂越好。我见过有人把 Text 封装成几十个参数,结果自己都记不住。保持简洁,够用就行。这个组件我用了两年,基本覆盖了 90% 的文本场景。

下一章我们会讲 Image 组件的封装,到时候会涉及图片缓存、占位图、圆角裁剪等实战技巧。嗯,敬请期待。