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 点击事件回调的优雅处理
点击事件看起来简单,但有几个细节要注意:
- 防重复点击:用户手抖点两下,回调可能触发两次
- 点击态反馈:文字颜色变暗或者背景闪一下,让用户知道点到了
我一般这样处理:
@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 组件的封装,到时候会涉及图片缓存、占位图、圆角裁剪等实战技巧。嗯,敬请期待。