HarmonyOS开发实战:笔友-@Extend/@Styles 样式复用与主题系统
前言
在 ArkUI 声明式开发范式中,样式复用是提升代码可维护性和 UI 一致性的关键手段。@Styles和@Extend两个装饰器分别从“通用样式集合“和“组件特定扩展“两个维度,为开发者提供了样式复用的能力。
本文将以开源鸿蒙笔友通信应用 xiexin 的Constants.ets中的AppColors设计令牌和各页面中的样式代码为蓝本,详细剖析@Styles/@Extend的语法、使用场景,以及它们如何与设计令牌(Design Tokens)配合,构建可维护的样式系统。
提示:本文假设你已经了解 ArkUI 声明式开发的基本概念。如果还不熟悉,建议先阅读前十一篇文章。
一、样式复用的两个维度
ArkUI 提供了两种样式复用机制,分别解决不同的问题:
| 装饰器 | 作用范围 | 适用场景 |
|---|---|---|
@Styles | 通用样式集合 | 跨组件共享的样式规则 |
@Extend | 特定组件扩展 | 针对特定组件类型的样式扩展 |
1.1 它们的区别
// @Styles:通用样式(不限定组件类型) @Styles function CommonShadow() { .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) } // @Extend:特定组件样式(限定 Text 组件) @Extend(Text) function TitleText() { .fontSize(16) .fontColor(AppColors.TEXT_PRIMARY) .fontWeight(FontWeight.Medium) }二、xiexin 的样式现状
2.1 当前样式组织方式
xiexin 当前没有使用@Styles或@Extend,而是通过直接属性调用和设计令牌来管理样式:
// Constants.ets — AppColors 设计令牌 export class AppColors { static readonly PRIMARY_BG: string = '#FAF6F0'; static readonly SECONDARY_BG: string = '#F0EBE3'; static readonly PAPER_BG: string = '#FFFDF7'; static readonly PRIMARY: string = '#8B6914'; static readonly SECONDARY: string = '#C4956A'; static readonly TEXT_PRIMARY: string = '#2D2A26'; static readonly TEXT_SECONDARY: string = '#7A746B'; static readonly ACCENT: string = '#B8860B'; static readonly WAITING: string = '#6B8E6B'; static readonly DISABLED: string = '#C8C2B8'; static readonly DIVIDER: string = '#E8E2D8'; static readonly WHITE: string = '#FFFFFF'; static readonly CARD_BG: string = '#FFFFFF'; static readonly AMBER_LIGHT: string = '#FFF8E7'; static readonly SUCCESS: string = '#4CAF50'; static readonly AMBER_OPACITY: string = '#1A8B6914'; }2.2 直接属性调用的弊端
// 反例:重复的样式代码 // PenPalCard Row() .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) // LetterCard Row() .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 })这两段代码几乎完全相同,却散落在两个不同的@Builder函数中。这就是@Styles和@Extend要解决的问题。
三、@Styles 装饰器:通用样式集合
3.1 @Styles 的基本用法
@Styles装饰器用于定义一组通用的样式规则,可以被多个组件共享。
全局 @Styles
@Styles function CardShadow() { .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) } @Styles function TextPrimary() { .fontSize(16) .fontColor(AppColors.TEXT_PRIMARY) .fontWeight(FontWeight.Medium) }组件内 @Styles
@Component struct MyComponent { @Styles CardStyle() { .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) } build() { Row() { /* ... */ } .CardStyle() // 调用组件内 @Styles } }3.2 @Styles 的核心特性
| 特性 | 说明 |
|---|---|
| 不限定组件 | 任何组件都可以调用 |
| 不可使用参数 | @Styles函数不能带参数 |
| 不可使用状态 | 不能访问this的@State变量 |
| 全局可导出 | 全局@Styles可以被export导出 |
3.3 @Styles 在 xiexin 中的应用示例
如果 xiexin 使用@Styles,可以对重复的卡片样式进行提取:
// 全局 @Styles 函数 @Styles function CardBase() { .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) } @Styles function CardShadow() { .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) } @Styles function SmallChip() { .fontSize(11) .borderRadius(8) .padding({ left: 6, right: 6, top: 2, bottom: 2 }) }然后在组件中使用:
@Builder PenPalCard(pal: PenPal) { Row() { // 笔友卡片内容 } .CardBase() .CardShadow() .onClick(() => { router.pushUrl({ url: 'pages/PenPalDetailPage', params: { penPalId: pal.id } }) }) } @Builder LetterCard(letter: Letter) { Row() { // 信件卡片内容 } .CardBase() .CardShadow() .onClick(() => { router.pushUrl({ url: 'pages/ReadLetterPage', params: { letterId: letter.id } }) }) }提示:
@Styles函数不能带参数,适用于“统一所有卡片外观“的场景。如果不同卡片需要不同的边距或颜色,应该使用@Extend。
四、@Extend 装饰器:特定组件扩展
4.1 @Extend 的基本用法
@Extend装饰器用于针对特定组件类型扩展样式。
// 针对 Text 组件的扩展 @Extend(Text) function TitleText() { .fontSize(16) .fontColor(AppColors.TEXT_PRIMARY) .fontWeight(FontWeight.Medium) } // 针对 Text 组件的扩展(带参数) @Extend(Text) function StatusText(color: string) { .fontSize(11) .fontColor(color) .fontWeight(FontWeight.Medium) .borderRadius(10) .padding({ left: 8, right: 8, top: 3, bottom: 3 }) }4.2 @Extend 的核心特性
| 特性 | 说明 |
|---|---|
| 组件限定 | 只能针对特定组件类型定义 |
| 支持参数 | 可以带参数,动态生成样式 |
| 全局函数 | 只能定义为全局函数 |
| 链式调用 | 多个@Extend可以链式调用 |
4.3 @Extend 在 xiexin 中的应用示例
xiexin 中重复次数最多的样式是Text组件的各种排版样式:
// 针对 Text 组件的扩展 @Extend(Text) function TitleText() { .fontSize(16) .fontColor(AppColors.TEXT_PRIMARY) .fontWeight(FontWeight.Medium) } @Extend(Text) function SubtitleText() { .fontSize(12) .fontColor(AppColors.TEXT_SECONDARY) } @Extend(Text) function PrimaryButton() { .fontSize(14) .fontColor(AppColors.WHITE) .backgroundColor(AppColors.PRIMARY) .borderRadius(24) .height(40) .width(140) } // 针对 Row 组件的扩展 @Extend(Row) function CardContainer() { .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) }在组件中使用:
// 使用 @Extend 简化样式代码 @Builder PenPalCard(pal: PenPal) { Row() { AvatarComponent({ name: pal.name, avatarSize: 48, fontSize: 20 }) .margin({ right: 14 }) Column({ space: 4 }) { Text(pal.name).TitleText() // 调用 @Extend(Text) 扩展 Text(`认识 ${pal.daysSinceMet} 天 · 通信 ${pal.totalLetters} 封`) .SubtitleText() } .layoutWeight(1) } .CardContainer() // 调用 @Extend(Row) 扩展 .onClick(() => { router.pushUrl({ url: 'pages/PenPalDetailPage', params: { penPalId: pal.id } }) }) }五、@Styles vs @Extend 对比
5.1 选择决策树
需要复用的样式是: ├── 跨多种组件类型共享 → @Styles │ └── 示例:卡片阴影、圆角、背景色 └── 针对特定组件类型 → @Extend └── 示例:Text 字号、Button 形状5.2 对比表格
| 维度 | @Styles | @Extend |
|---|---|---|
| 组件限定 | 否 | 是 |
| 参数支持 | 否 | 是 |
| 作用范围 | 全局/组件内 | 全局 |
| 调用方式 | 链式调用 | 链式调用 |
| 可导出 | 是 | 是 |
| 典型场景 | 卡片阴影、基础布局 | Text 字号、Button 样式 |
5.3 组合使用
@Styles和@Extend可以组合使用,实现更精细的样式管理:
// 基础布局样式(跨组件) @Styles function FlexCenter() { .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } @Styles function FullWidth() { .width('100%') } // 特定组件样式 @Extend(Text) function TitleText() { .fontSize(16) .fontColor(AppColors.TEXT_PRIMARY) .fontWeight(FontWeight.Medium) } // 组合使用 Row() .FullWidth() .FlexCenter() .height(48)六、stateStyles 多态样式
6.1 stateStyles 的基本用法
stateStyles是 ArkUI 提供的多态样式机制,允许组件在不同状态下应用不同的样式:
@Entry @Component struct Index { @Styles normalStyle() { .backgroundColor(AppColors.CARD_BG) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) } @Styles pressedStyle() { .backgroundColor(AppColors.SECONDARY_BG) .shadow({ radius: 3, color: '#0A000000', offsetX: 0, offsetY: 1 }) } build() { Row() .stateStyles({ normal: this.normalStyle, pressed: this.pressedStyle, // focused: this.focusedStyle // 可选 }) } }6.2 stateStyles 在 xiexin 中的应用
如果 xiexin 使用stateStyles,卡片按压效果可以这样实现:
@Styles cardNormal() { .backgroundColor(AppColors.CARD_BG) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) } @Styles cardPressed() { .backgroundColor(AppColors.SECONDARY_BG) .shadow({ radius: 3, color: '#0A000000', offsetX: 0, offsetY: 1 }) } @Builder PenPalCard(pal: PenPal) { Row() { // 内容 } .CardContainer() .stateStyles({ normal: this.cardNormal, pressed: this.cardPressed, }) .onClick(() => { router.pushUrl({ url: 'pages/PenPalDetailPage', params: { penPalId: pal.id } }) }) }提示:
stateStyles支持normal、pressed、focused三种状态。pressed状态在用户按下时触发,focused状态在组件获得焦点时触发。
七、@AnimatableExtend 可动画属性扩展
7.1 @AnimatableExtend 的基本用法
@AnimatableExtend装饰器用于定义可动画的属性,可以在动画过程中平滑过渡:
@AnimatableExtend(Text) function AnimatableOpacity() { .opacity() } @AnimatableExtend(Row) function AnimatableScale() { .scale() }7.2 @AnimatableExtend 与动画的配合
@AnimatableExtend(Text) function AnimatableFontSize() { .fontSize() } @Entry @Component struct AnimatedText { @State fontSize: number = 16; build() { Column() { Text('Hello') .AnimatableFontSize(this.fontSize) Button('放大') .onClick(() => { animateTo({ duration: 300 }, () => { this.fontSize = 32; }) }) } } }八、设计令牌与样式系统的整合
8.1 设计令牌 + @Styles/@Extend 三层架构
将设计令牌、@Styles、@Extend三层整合,可以构建完整的样式系统:
graph TD subgraph 第1层 设计令牌 C[AppColors 颜色常量] S[字号常量] M[间距常量] end subgraph 第2层 通用样式 ST[全局 @Styles 函数] EX[全局 @Extend 函数] end subgraph 第3层 组件样式 CP[组件内 @Styles] DS[stateStyles 多态样式] end C --> ST C --> EX ST --> CP EX --> DS8.2 样式文件组织建议
对于 xiexin 项目,建议将样式代码按以下方式组织:
entry/src/main/ets/ ├── common/ │ ├── AppColors.ets # 设计令牌:颜色 │ ├── Typography.ets # 设计令牌:字号、行高 │ └── Spacing.ets # 设计令牌:间距 ├── styles/ │ ├── GlobalStyles.ets # 全局 @Styles 函数 │ ├── TextStyles.ets # @Extend(Text) 扩展 │ └── LayoutStyles.ets # @Extend(Row/Column) 扩展 └── pages/ ├── Index.ets # 组件内 @Styles └── ... # 其他页面8.3 设计令牌 + @Extend 的完整示例
// 第1层:设计令牌 export class FontSizes { static readonly TITLE: number = 16; static readonly BODY: number = 14; static readonly CAPTION: number = 12; static readonly SMALL: number = 11; } // 第2层:@Extend 扩展 @Extend(Text) function TitleText() { .fontSize(FontSizes.TITLE) .fontColor(AppColors.TEXT_PRIMARY) .fontWeight(FontWeight.Medium) } @Extend(Text) function BodyText() { .fontSize(FontSizes.BODY) .fontColor(AppColors.TEXT_PRIMARY) } @Extend(Text) function CaptionText() { .fontSize(FontSizes.CAPTION) .fontColor(AppColors.TEXT_SECONDARY) } // 第3层:组件中使用 @Builder PenPalCard(pal: PenPal) { Row() { Column({ space: 4 }) { Text(pal.name).TitleText() // 笔友名:标题字号 Text(pal.signature).BodyText() // 签名:正文字号 Text(`认识 ${pal.daysSinceMet} 天`).CaptionText() // 天数:注解字号 } } }九、主题切换样式
9.1 深色模式适配
通过@Styles和@Extend配合 AppColors 设计令牌,可以方便地实现主题切换:
// 浅色模式 const lightColors = { PRIMARY_BG: '#FAF6F0', TEXT_PRIMARY: '#2D2A26', // ... }; // 深色模式 const darkColors = { PRIMARY_BG: '#1C1B1F', TEXT_PRIMARY: '#E6E1E5', // ... }; // 根据主题切换设计令牌 @Styles function CardBase() { .backgroundColor(AppColors.CARD_BG) .borderRadius(16) }9.2 运行时主题切换
@Entry @Component struct MyApp { @StorageProp('darkMode') darkMode: boolean = false; @Styles cardBase() { .backgroundColor(this.darkMode ? '#2D2D2D' : '#FFFFFF') .borderRadius(16) } build() { Row() { // 卡片内容 } .cardBase() } }提示:
@Styles虽然不能带参数,但可以通过@StorageProp访问全局状态,实现主题切换样式。
十、样式系统的性能优化
10.1 @Styles 与 @Extend 的渲染开销
@Styles和@Extend在编译时会被展开为普通的属性调用,不会带来额外的运行时开销:
// 编译前 Row() .CardBase() .CardShadow() // 编译后(等价) Row() .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 })10.2 避免过度抽象
// 过度抽象:仅仅封装了一个属性 @Styles function Width100() { .width('100%') } // 合理的使用:封装了多个属性的组合 @Styles function CardContainer() { .width('100%') .padding(16) .backgroundColor(AppColors.CARD_BG) .borderRadius(16) .shadow({ radius: 6, color: '#0A000000', offsetX: 0, offsetY: 2 }) }总结
本文详细剖析了 HarmonyOS ArkUI 的@Styles/@Extend样式复用机制,重点讲解了它们与设计令牌AppColors的配合使用,以及如何通过stateStyles实现多态样式。
理解样式复用机制的关键是把握“三层架构“:设计令牌(AppColors)→ 通用样式(@Styles)→ 组件特定扩展(@Extend)。这三层各司其职,共同构成了可维护的样式系统。
下一篇文章我们将深入@Observed和@ObjectLink装饰器,剖析 xiexin 中如何通过嵌套对象响应式追踪实现高效的状态更新。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- HarmonyOS @Styles 装饰器:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-style
- HarmonyOS @Extend 装饰器:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-extend
- HarmonyOS stateStyles 多态样式:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-statestyles
- HarmonyOS @AnimatableExtend 装饰器:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-animatable-extend
- HarmonyOS 资源分类与访问:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/resource-categories-and-access
- HarmonyOS 深色模式适配:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-dark-light-adaptation
- HarmonyOS 组件扩展概述:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-extend-components-overview
- HarmonyOS 自定义组件复用:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-component-reusable