ArkTS 样式结构重用三大装饰器详解

概述

在 ArkTS 开发中,有三个核心装饰器用于提高代码复用性:

  • @Styles - 样式复用(仅属性)
  • @Extend - 组件扩展(仅属性)
  • @Builder - 结构复用(UI 结构)

这三个装饰器各有特点,合理使用可以大幅提升开发效率和代码可维护性。


一、@Styles 装饰器

1.1 基本概念

@Styles 用于提取公共属性样式,将多个通用样式属性封装成一个方法,实现样式的复用。

1.2 核心特点

  • ✅ 提取公共样式属性
  • ✅ 可全局定义或组件内定义
  • ✅ 支持状态变量
  • 不支持参数传递
  • 只能设置通用属性(所有组件共有的属性)
  • 不能包含 UI 结构

1.3 语法格式

// 全局定义(使用 function 关键字)
@Styles function globalStyleName() {
  .width(200)
  .height(100)
  .backgroundColor(Color.Red)
}

// 组件内定义(不使用 function 关键字)
@Component
struct MyComponent {
  @Styles localStyleName() {
    .width(200)
    .height(100)
    .backgroundColor(Color.Blue)
  }
}

1.4 使用示例

// 全局样式定义
@Styles function commonCardStyle() {
  .width('90%')
  .height(120)
  .backgroundColor(Color.White)
  .borderRadius(12)
  .padding(16)
  .shadow({ radius: 8, color: '#00000020' })
}

@Entry
@Component
struct StylesDemo {
  // 组件内样式定义
  @Styles primaryTextStyle() {
    .fontSize(16)
    .fontColor('#333333')
    .fontWeight(FontWeight.Medium)
  }

  build() {
    Column({ space: 20 }) {
      // 使用全局样式
      Column() {
        Text('卡片内容')
      }
      .commonCardStyle()

      // 使用组件内样式
      Text('标题文字')
        .primaryTextStyle()

      // 样式可以叠加和覆盖
      Text('特殊文字')
        .primaryTextStyle()
        .fontColor(Color.Red)  // 覆盖颜色
    }
    .width('100%')
    .padding(20)
  }
}

1.5 使用限制

// ❌ 错误:不能传递参数
@Styles function wrongStyle(color: Color) {  // 编译错误
  .backgroundColor(color)
}

// ❌ 错误:不能设置组件特有属性
@Styles function wrongTextStyle() {
  .text('Hello')  // 编译错误,text 不是通用属性
}

// ✅ 正确:只使用通用属性
@Styles function correctStyle() {
  .width(100)
  .height(100)
  .backgroundColor(Color.Blue)
}

二、@Extend 装饰器

2.1 基本概念

@Extend 用于扩展特定组件的样式,可以为指定组件类型添加自定义样式方法,并且支持参数传递

2.2 核心特点

  • 支持参数传递(核心优势)
  • ✅ 参数可以是状态变量
  • ✅ 可设置组件特有属性
  • ✅ 针对特定组件类型
  • 只能全局定义
  • 不能包含 UI 结构
  • ❌ 只能扩展一个组件类型

2.3 语法格式

// 必须在组件外部定义
@Extend(ComponentType) function extendMethodName(param1: type1, param2: type2) {
  .attribute1(param1)
  .attribute2(param2)
}

2.4 使用示例

基础参数化样式
// 扩展 Text 组件
@Extend(Text) function fancyText(size: number, color: ResourceColor, weight: FontWeight) {
  .fontSize(size)
  .fontColor(color)
  .fontWeight(weight)
  .textAlign(TextAlign.Center)
  .lineHeight(size * 1.5)
}

// 扩展 Button 组件
@Extend(Button) function themeButton(bgColor: ResourceColor, size: 'small' | 'medium' | 'large') {
  .backgroundColor(bgColor)
  .borderRadius(8)
  .width(size === 'small' ? 80 : size === 'medium' ? 120 : 160)
  .height(size === 'small' ? 32 : size === 'medium' ? 40 : 48)
}

@Entry
@Component
struct ExtendDemo {
  build() {
    Column({ space: 20 }) {
      // 使用扩展方法
      Text('大标题')
        .fancyText(24, '#333333', FontWeight.Bold)

      Text('副标题')
        .fancyText(18, '#666666', FontWeight.Medium)

      Button('小按钮')
        .themeButton('#007DFF', 'small')

      Button('大按钮')
        .themeButton('#FF6B6B', 'large')
    }
    .width('100%')
    .padding(20)
  }
}
支持状态变量
@Extend(Column) function cardContainer(
  bgColor: ResourceColor,
  isSelected: boolean,
  elevation: number
) {
  .backgroundColor(bgColor)
  .borderRadius(12)
  .padding(16)
  .border({
    width: isSelected ? 2 : 0,
    color: '#007DFF'
  })
  .shadow({
    radius: elevation,
    color: isSelected ? '#007DFF40' : '#00000020'
  })
}

@Entry
@Component
struct ExtendStateDemo {
  @State selectedIndex: number = 0
  @State items: string[] = ['选项1', '选项2', '选项3']

  build() {
    Column({ space: 16 }) {
      ForEach(this.items, (item: string, index: number) => {
        Column() {
          Text(item)
            .fontSize(16)
        }
        .width('100%')
        .height(80)
        .cardContainer(
          Color.White,
          this.selectedIndex === index,  // 状态变量
          this.selectedIndex === index ? 16 : 8  // 根据状态动态调整
        )
        .onClick(() => {
          this.selectedIndex = index
        })
      })
    }
    .width('100%')
    .padding(20)
  }
}
扩展事件和手势
@Extend(Text) function clickableText(
  normalColor: ResourceColor,
  pressedColor: ResourceColor,
  onClick: () => void
) {
  .fontColor(normalColor)
  .onClick(onClick)
  .stateStyles({
    normal: {
      .fontColor(normalColor)
    },
    pressed: {
      .fontColor(pressedColor)
    }
  })
}

@Entry
@Component
struct ExtendEventDemo {
  @State message: string = ''

  build() {
    Column({ space: 20 }) {
      Text('点击我')
        .fontSize(18)
        .clickableText(
          '#007DFF',
          '#0056B3',
          () => {
            this.message = '文字被点击了'
          }
        )

      Text(this.message)
        .fontSize(14)
        .fontColor('#999')
    }
    .width('100%')
    .padding(20)
  }
}

三、@Builder 装饰器

3.1 基本概念

@Builder 用于UI 结构的复用,可以将一段 UI 结构封装成方法,实现复杂视图的重复使用。

3.2 核心特点

  • 复用 UI 结构(核心优势)
  • ✅ 支持参数传递
  • ✅ 可全局定义或组件内定义
  • ✅ 支持状态变量
  • ✅ 可以包含逻辑判断
  • ✅ 可以嵌套其他 @Builder
  • ✅ 支持尾随闭包语法

3.3 语法格式

// 全局定义
@Builder function globalBuilderName(params) {
  // UI 结构
}

// 组件内定义
@Component
struct MyComponent {
  @Builder localBuilderName(params) {
    // UI 结构
  }
}

3.4 使用示例

基础 UI 结构复用
// 全局 Builder
@Builder function IconTextItem(icon: Resource, text: string, color: ResourceColor) {
  Row({ space: 8 }) {
    Image(icon)
      .width(24)
      .height(24)
    Text(text)
      .fontSize(16)
      .fontColor(color)
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#F5F5F5')
  .borderRadius(8)
}

@Entry
@Component
struct BuilderDemo {
  build() {
    Column({ space: 12 }) {
      IconTextItem($r('app.media.home'), '首页', '#333333')
      IconTextItem($r('app.media.message'), '消息', '#666666')
      IconTextItem($r('app.media.profile'), '我的', '#999999')
    }
    .width('100%')
    .padding(20)
  }
}
组件内 Builder(访问组件状态)
@Entry
@Component
struct BuilderStateDemo {
  @State count: number = 0
  @State isExpanded: boolean = false

  // 组件内 Builder 可以访问组件的状态变量
  @Builder CounterCard() {
    Column({ space: 16 }) {
      Text(`当前计数: ${this.count}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Row({ space: 12 }) {
        Button('-')
          .width(60)
          .onClick(() => this.count--)

        Button('+')
          .width(60)
          .onClick(() => this.count++)
      }

      if (this.isExpanded) {
        Text('详细信息区域')
          .fontSize(14)
          .fontColor('#666')
          .padding(12)
          .backgroundColor('#F0F0F0')
          .borderRadius(4)
      }

      Button(this.isExpanded ? '收起' : '展开')
        .onClick(() => {
          this.isExpanded = !this.isExpanded
        })
    }
    .width('100%')
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({ radius: 8, color: '#00000020' })
  }

  build() {
    Column() {
      this.CounterCard()
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}
复杂列表项 Builder
interface ListItemData {
  title: string
  subtitle: string
  icon: Resource
  badge?: number
  isNew?: boolean
}

@Builder function ComplexListItem(data: ListItemData, onClick: () => void) {
  Row({ space: 12 }) {
    // 图标
    Image(data.icon)
      .width(48)
      .height(48)
      .borderRadius(24)

    // 文字信息
    Column({ space: 4 }) {
      Row({ space: 8 }) {
        Text(data.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')

        if (data.isNew) {
          Text('NEW')
            .fontSize(10)
            .fontColor(Color.White)
            .backgroundColor('#FF6B6B')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(4)
        }
      }

      Text(data.subtitle)
        .fontSize(14)
        .fontColor('#999999')
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    // 徽章
    if (data.badge && data.badge > 0) {
      Text(data.badge > 99 ? '99+' : data.badge.toString())
        .fontSize(12)
        .fontColor(Color.White)
        .backgroundColor('#FF3B30')
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        .borderRadius(10)
        .constraintSize({ minWidth: 20 })
        .textAlign(TextAlign.Center)
    }

    Image($r('app.media.arrow_right'))
      .width(20)
      .height(20)
  }
  .width('100%')
  .padding(16)
  .backgroundColor(Color.White)
  .borderRadius(12)
  .onClick(onClick)
}

@Entry
@Component
struct ComplexBuilderDemo {
  @State items: ListItemData[] = [
    { title: '消息中心', subtitle: '3条新消息', icon: $r('app.media.message'), badge: 3, isNew: false },
    { title: '系统通知', subtitle: '重要更新', icon: $r('app.media.notification'), badge: 1, isNew: true },
    { title: '个人设置', subtitle: '账号与安全', icon: $r('app.media.settings'), badge: 0 }
  ]

  build() {
    Column({ space: 12 }) {
      ForEach(this.items, (item: ListItemData, index: number) => {
        ComplexListItem(item, () => {
          console.log(`点击了: ${item.title}`)
        })
      })
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}
尾随闭包语法(容器组件)
@Builder function CustomCard(title: string, @BuilderParam content: () => void) {
  Column({ space: 12 }) {
    // 卡片标题
    Text(title)
      .fontSize(18)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
      .width('100%')

    Divider()

    // 动态内容区域
    content()
  }
  .width('100%')
  .padding(16)
  .backgroundColor(Color.White)
  .borderRadius(12)
  .shadow({ radius: 8, color: '#00000020' })
}

@Entry
@Component
struct BuilderParamDemo {
  @State username: string = '张三'
  @State score: number = 98

  build() {
    Column({ space: 20 }) {
      // 使用尾随闭包传入自定义内容
      CustomCard('用户信息') {
        Column({ space: 8 }) {
          Text(`姓名: ${this.username}`)
            .fontSize(16)
          Text(`分数: ${this.score}`)
            .fontSize(16)
            .fontColor('#007DFF')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
      }

      CustomCard('操作按钮') {
        Row({ space: 12 }) {
          Button('编辑')
            .width(100)
          Button('删除')
            .width(100)
            .backgroundColor('#FF6B6B')
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}
条件渲染 Builder
@Entry
@Component
struct ConditionalBuilderDemo {
  @State isLoading: boolean = true
  @State hasError: boolean = false
  @State data: string[] = []

  @Builder LoadingView() {
    Column() {
      LoadingProgress()
        .width(50)
        .height(50)
      Text('加载中...')
        .fontSize(14)
        .fontColor('#999')
        .margin({ top: 12 })
    }
    .width('100%')
    .height(200)
    .justifyContent(FlexAlign.Center)
  }

  @Builder ErrorView() {
    Column() {
      Image($r('app.media.error'))
        .width(80)
        .height(80)
      Text('加载失败')
        .fontSize(16)
        .fontColor('#FF6B6B')
        .margin({ top: 12 })
      Button('重试')
        .margin({ top: 20 })
        .onClick(() => {
          this.loadData()
        })
    }
    .width('100%')
    .height(200)
    .justifyContent(FlexAlign.Center)
  }

  @Builder EmptyView() {
    Column() {
      Image($r('app.media.empty'))
        .width(100)
        .height(100)
      Text('暂无数据')
        .fontSize(14)
        .fontColor('#999')
        .margin({ top: 12 })
    }
    .width('100%')
    .height(200)
    .justifyContent(FlexAlign.Center)
  }

  @Builder ContentView() {
    Column({ space: 12 }) {
      ForEach(this.data, (item: string) => {
        Text(item)
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(8)
      })
    }
    .width('100%')
  }

  @Builder ContentContainer() {
    if (this.isLoading) {
      this.LoadingView()
    } else if (this.hasError) {
      this.ErrorView()
    } else if (this.data.length === 0) {
      this.EmptyView()
    } else {
      this.ContentView()
    }
  }

  loadData() {
    this.isLoading = true
    this.hasError = false

    // 模拟网络请求
    setTimeout(() => {
      this.isLoading = false
      this.data = ['数据1', '数据2', '数据3']
    }, 2000)
  }

  aboutToAppear() {
    this.loadData()
  }

  build() {
    Column() {
      this.ContentContainer()
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}

四、三者对比总结

4.1 核心区别表

对比维度 @Styles @Extend @Builder
主要用途 样式属性复用 组件样式扩展 UI 结构复用
定义位置 全局或组件内 仅全局 全局或组件内
参数支持 ❌ 不支持 ✅ 支持 ✅ 支持
状态变量 ✅ 支持内部使用 ✅ 支持作为参数 ✅ 支持
组件限定 ❌ 通用属性 ✅ 指定组件类型 ❌ 不限定
UI 结构 ❌ 不能包含 ❌ 不能包含 ✅ 可以包含
特有属性 ❌ 不能设置 ✅ 可以设置 ✅ 可以设置
尾随闭包 ❌ 不支持 ❌ 不支持 ✅ 支持
适用场景 固定的公共样式 参数化的组件样式 复杂 UI 结构复用
灵活性
复杂度

4.2 使用场景对比

// 场景1: 固定的样式组合 → 使用 @Styles
@Styles function cardShadow() {
  .shadow({ radius: 8, color: '#00000020', offsetY: 2 })
  .backgroundColor(Color.White)
  .borderRadius(12)
}

// 场景2: 需要参数的样式 → 使用 @Extend
@Extend(Text) function themeText(level: 1 | 2 | 3) {
  .fontSize(level === 1 ? 24 : level === 2 ? 18 : 14)
  .fontWeight(level === 1 ? FontWeight.Bold : FontWeight.Medium)
  .fontColor('#333333')
}

// 场景3: 复杂的UI结构 → 使用 @Builder
@Builder function UserCard(name: string, avatar: Resource, status: 'online' | 'offline') {
  Row({ space: 12 }) {
    Stack() {
      Image(avatar)
        .width(48)
        .height(48)
        .borderRadius(24)
      Circle({ width: 12, height: 12 })
        .fill(status === 'online' ? '#52C41A' : '#999')
        .position({ x: 36, y: 36 })
    }
    Column({ space: 4 }) {
      Text(name)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
      Text(status === 'online' ? '在线' : '离线')
        .fontSize(12)
        .fontColor('#999')
    }
    .alignItems(HorizontalAlign.Start)
  }
  .padding(12)
  .cardShadow()  // 组合使用 @Styles
}

@Entry
@Component
struct ComparisonDemo {
  build() {
    Column({ space: 20 }) {
      // 使用 @Styles
      Column()
        .width('100%')
        .height(100)
        .cardShadow()

      // 使用 @Extend
      Text('一级标题')
        .themeText(1)

      // 使用 @Builder
      UserCard('张三', $r('app.media.avatar'), 'online')
    }
    .width('100%')
    .padding(20)
  }
}

五、组合使用最佳实践

5.1 分层设计

// 第一层:基础样式 (@Styles)
@Styles function baseCard() {
  .backgroundColor(Color.White)
  .borderRadius(12)
  .padding(16)
}

@Styles function shadowSmall() {
  .shadow({ radius: 4, color: '#00000010' })
}

@Styles function shadowLarge() {
  .shadow({ radius: 12, color: '#00000030' })
}

// 第二层:组件扩展 (@Extend)
@Extend(Column) function card(elevation: 'small' | 'large') {
  .baseCard()
  if (elevation === 'small') {
    .shadowSmall()
  } else {
    .shadowLarge()
  }
}

@Extend(Text) function title(level: number) {
  .fontSize(24 - level * 2)
  .fontWeight(level === 1 ? FontWeight.Bold : FontWeight.Medium)
  .fontColor('#333333')
}

// 第三层:UI结构 (@Builder)
@Builder function InfoCard(
  titleText: string,
  content: string,
  elevation: 'small' | 'large'
) {
  Column({ space: 12 }) {
    Text(titleText)
      .title(1)
      .width('100%')

    Text(content)
      .fontSize(14)
      .fontColor('#666666')
      .width('100%')
  }
  .width('100%')
  .card(elevation)
}

@Entry
@Component
struct LayeredDesignDemo {
  build() {
    Column({ space: 20 }) {
      InfoCard('标题1', '这是一个小阴影卡片', 'small')
      InfoCard('标题2', '这是一个大阴影卡片', 'large')
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}

5.2 复杂表单场景

// 样式定义
@Styles function inputContainerStyle() {
  .width('100%')
  .padding(12)
  .backgroundColor('#F5F5F5')
  .borderRadius(8)
}

// 组件扩展
@Extend(TextInput) function formInput(placeholder: string) {
  .placeholder(placeholder)
  .backgroundColor(Color.Transparent)
  .fontSize(16)
  .padding(0)
}

// UI 结构
@Builder function FormField(
  label: string,
  placeholder: string,
  value: string,
  onChange: (value: string) => void
) {
  Column({ space: 8 }) {
    Text(label)
      .fontSize(14)
      .fontColor('#666666')
      .width('100%')

    Column() {
      TextInput({ text: value })
        .formInput(placeholder)
        .onChange(onChange)
    }
    .inputContainerStyle()
  }
  .width('100%')
}

@Entry
@Component
struct FormDemo {
  @State username: string = ''
  @State email: string = ''
  @State phone: string = ''

  build() {
    Column({ space: 20 }) {
      Text('用户注册')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      FormField('用户名', '请输入用户名', this.username, (value) => {
        this.username = value
      })

      FormField('邮箱', '请输入邮箱', this.email, (value) => {
        this.email = value
      })

      FormField('手机号', '请输入手机号', this.phone, (value) => {
        this.phone = value
      })

      Button('提交')
        .width('100%')
        .height(48)
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor(Color.White)
  }
}

六、性能优化建议

6.1 @Builder 的性能考虑

// ❌ 避免:每次都创建新的 Builder
@Entry
@Component
struct BadExample {
  @State items: string[] = ['A', 'B', 'C']

  build() {
    Column() {
      ForEach(this.items, (item: string) => {
        // 每次都创建新的闭包,性能差
        this.ItemBuilder(item, () => {
          console.log(item)
        })
      })
    }
  }

  @Builder ItemBuilder(text: string, onClick: () => void) {
    Text(text).onClick(onClick)
  }
}

// ✅ 推荐:复用 Builder
@Entry
@Component
struct GoodExample {
  @State items: string[] = ['A', 'B', 'C']

  handleItemClick(item: string) {
    console.log(item)
  }

  @Builder ItemBuilder(text: string) {
    Text(text)
      .onClick(() => {
        this.handleItemClick(text)
      })
  }

  build() {
    Column() {
      ForEach(this.items, (item: string) => {
        this.ItemBuilder(item)
      })
    }
  }
}

6.2 合理拆分 Builder

// ✅ 将大的 Builder 拆分成小的独立部分
@Component
struct OptimizedList {
  @State data: Array<any> = []

  // 头部独立
  @Builder HeaderBuilder() {
    Row() {
      Text('标题')
    }
    .height(56)
  }

  // 列表项独立
  @Builder ListItemBuilder(item: any) {
    Row() {
      Text(item.name)
    }
  }

  // 底部独立
  @Builder FooterBuilder() {
    Row() {
      Text('已加载全部')
    }
  }

  build() {
    Column() {
      this.HeaderBuilder()

      List() {
        ForEach(this.data, (item: any) => {
          ListItem() {
            this.ListItemBuilder(item)
          }
        })
      }

      this.FooterBuilder()
    }
  }
}

七、最佳实践总结

7.1 选择决策树

需要复用吗?
├─ 否 → 直接编写代码
└─ 是
   ├─ 只是样式属性?
   │  ├─ 需要参数?
   │  │  ├─ 是 → 使用 @Extend
   │  │  └─ 否 → 使用 @Styles
   │  └─ 针对特定组件? → 使用 @Extend
   └─ 包含UI结构? → 使用 @Builder

7.2 命名规范

// @Styles: 描述性名称 + Style 后缀
@Styles function primaryButtonStyle() {}
@Styles function cardShadowStyle() {}

// @Extend: 组件类型 + 功能描述
@Extend(Button) function themeButton() {}
@Extend(Text) function headlineText() {}

// @Builder: 功能描述 + Builder/View 后缀
@Builder function UserCardBuilder() {}
@Builder function LoadingView() {}

7.3 注意事项

  1. @Styles

    • 只用于纯样式属性
    • 全局定义使用 function,组件内不使用
    • 不要尝试传递参数
  2. @Extend

    • 必须在组件外部定义
    • 明确指定组件类型
    • 合理使用参数,避免过多参数
  3. @Builder

    • 避免过于复杂的嵌套
    • 合理拆分大型 Builder
    • 注意状态管理和性能
  4. 组合使用

    • @Styles 提供基础样式
    • @Extend 提供参数化扩展
    • @Builder 组织 UI 结构
    • 保持清晰的分层

八、总结

装饰器 核心价值 典型场景 记忆口诀
@Styles 样式属性复用 固定的样式组合 “样式打包”
@Extend 组件样式参数化扩展 需要动态调整的样式 “样式带参”
@Builder UI 结构复用 复杂视图结构重复使用 “结构模板”

使用原则

  • 简单样式 → @Styles
  • 参数化样式 → @Extend
  • 结构复用 → @Builder
  • 复杂场景 → 三者组合

掌握这三个装饰器的特点和使用场景,可以大幅提升 ArkTS 开发效率和代码质量!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐