第8篇:组件化开发——创建可复用组件

本课目标:掌握@Component装饰器和组件封装,能创建可复用组件
**作者:**中文编程倡导者—— 李金雨
预计课时:3课时(135分钟)
难度等级:⭐⭐⭐⭐(高级)


一、开篇引入

1.1 从"重复造轮子"到"拿来就用"

想象你要搭建一个乐高城市:

  • 你需要很多房子
  • 每个房子都有门、窗、屋顶
  • 你不会每次都重新设计,而是用一个"房子模板",复制多份

组件化开发就是这个思想!

1.2 为什么要组件化?

不用组件化的问题

// 页面A需要用户信息卡片
Column() {
  Image(头像)
  Text(姓名)
  Text(年龄)
}

// 页面B也需要用户信息卡片
Column() {
  Image(头像)
  Text(姓名)
  Text(年龄)
}

// 页面C还需要... 重复写N次!

用组件化的好处

// 定义一次
@Component
struct 用户卡片 {
  // ...
}

// 到处使用
用户卡片()
用户卡片()
用户卡片()

1.3 本课目标

今天我们要学习:

  1. 什么是组件
  2. 怎么创建自定义组件
  3. 组件间怎么传递数据(@Prop)
  4. 组件怎么发送事件
  5. 实战:按钮组件、卡片组件、弹窗组件

1.4 预期成果

完成本课后,你能创建这样的组件库:

// 通用按钮
<自定义按钮 文字="确定" 类型="主要" 点击={() => {}} />

// 信息卡片
<信息卡片 标题="xxx" 内容="xxx" 图片="xxx" />

// 确认弹窗
<确认弹窗 显示={true} 标题="提示" 内容="确定删除吗?" />

二、概念讲解

2.1 什么是组件?

定义

组件是界面的独立、可复用的组成部分。

就像乐高积木块,每个组件:

  • 有自己的结构和样式
  • 可以接收输入(属性)
  • 可以发出输出(事件)
  • 可以嵌套组合
组件的层次
应用
├── 页面A
│   ├── 头部组件
│   │   ├── Logo组件
│   │   └── 导航组件
│   ├── 内容组件
│   │   ├── 卡片组件 × 3
│   │   └── 列表组件
│   └── 底部组件
└── 页面B
    ├── 头部组件(复用)
    └── ...

2.2 @Component装饰器

创建组件
@Component
struct 组件名字 {
  build() {
    // 组件的界面代码
  }
}
部分 说明
@Component 装饰器,标记这是一个组件
struct 定义结构体
组件名字 组件的名称(用大驼峰命名)
build() 组件的界面组装方法
简单例子:问候组件
@Component
struct 问候组件 {
  build() {
    Column() {
      Text("你好!")
        .fontSize(24)
      Text("欢迎使用本应用")
        .fontSize(14)
        .fontColor("#999999")
    }
    .padding(20)
    .backgroundColor("#F5F5F5")
    .borderRadius(10)
  }
}

// 使用组件
@Entry
@Component
struct 主页面 {
  build() {
    Column() {
      问候组件()    // 使用自定义组件
      问候组件()    // 可以重复使用
    }
  }
}

2.3 @Prop——父组件向子组件传数据

什么是@Prop?

@Prop 让组件可以接收外部传入的数据

就像函数参数一样!

基本用法
@Component
struct 用户卡片 {
  @Prop 姓名: string      // 接收姓名
  @Prop 年龄: number      // 接收年龄
  @Prop 头像: string      // 接收头像

  build() {
    Row({ space: 15 }) {
      Text(this.头像)
        .fontSize(40)
      
      Column({ space: 5 }) {
        Text(this.姓名)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        Text(`${this.年龄}`)
          .fontSize(14)
          .fontColor("#999999")
      }
    }
    .padding(15)
    .backgroundColor("#FFFFFF")
    .borderRadius(10)
  }
}

// 使用组件,传入数据
@Entry
@Component
struct 主页面 {
  build() {
    Column({ space: 10 }) {
      用户卡片({ 
        姓名: "张三", 
        年龄: 15, 
        头像: "👦" 
      })
      
      用户卡片({ 
        姓名: "李四", 
        年龄: 16, 
        头像: "👧" 
      })
    }
  }
}
@Prop的特点
@Component
struct 子组件 {
  @Prop 数据: string    // 从父组件接收
  
  build() {
    Button(this.数据)
      .onClick(() => {
        // 可以修改本地副本,不会影响父组件
        this.数据 = "新值"
      })
  }
}
  • @Prop单向绑定:父→子
  • 子组件修改@Prop,不会同步到父组件

2.4 @Link——双向绑定

什么是@Link?

@Link 让父子组件双向同步数据。

父组件变,子组件变;子组件变,父组件也变。

基本用法
@Component
struct 计数器组件 {
  @Link 计数: number    // 双向绑定

  build() {
    Row({ space: 20 }) {
      Button("-")
        .onClick(() => {
          this.计数--    // 修改会影响父组件
        })
      
      Text(`${this.计数}`)
        .fontSize(30)
      
      Button("+")
        .onClick(() => {
          this.计数++    // 修改会影响父组件
        })
    }
  }
}

// 父组件
@Entry
@Component
struct 主页面 {
  @State 总数量: number = 0

  build() {
    Column() {
      Text(`当前总数:${this.总数量}`)
      
      // 使用$符号传递Link
      计数器组件({ 计数: $总数量 })
    }
  }
}

2.5 组件事件——子组件向父组件通信

什么是组件事件?

子组件通过回调函数通知父组件发生了什么事。

基本用法
@Component
struct 按钮组件 {
  @Prop 文字: string
  点击回调: () => void = () => {}    // 定义回调

  build() {
    Button(this.文字)
      .onClick(() => {
        this.点击回调()    // 触发回调
      })
  }
}

// 父组件
@Entry
@Component
struct 主页面 {
  @State 点击次数: number = 0

  build() {
    Column() {
      Text(`点击了${this.点击次数}`)
      
      按钮组件({
        文字: "点我",
        点击回调: () => {
          this.点击次数++    // 父组件处理事件
        }
      })
    }
  }
}
带参数的事件
@Component
struct 列表项组件 {
  @Prop 标题: string
  点击回调: (标题: string) => void = () => {}

  build() {
    Row() {
      Text(this.标题)
    }
    .onClick(() => {
      this.点击回调(this.标题)    // 传递参数
    })
  }
}

// 使用
列表项组件({
  标题: "项目1",
  点击回调: (标题: string) => {
    console.log("点击了:" + 标题)
  }
})

2.6 @Builder——构建器函数

什么是@Builder?

@Builder 是一种轻量级的组件定义方式,适合简单的、不复用的界面片段。

基本用法
@Entry
@Component
struct 主页面 {
  @State 用户名: string = "张三"

  build() {
    Column() {
      // 使用@Builder
      this.用户卡片(this.用户名)
    }
  }
  
  // 定义@Builder
  @Builder
  用户卡片(名字: string) {
    Row() {
      Text("👤")
      Text(名字)
    }
    .padding(15)
    .backgroundColor("#F5F5F5")
  }
}
@Builder vs @Component
特性 @Component @Builder
复用性 高(可到处使用) 低(只在当前组件内)
复杂度 适合复杂组件 适合简单片段
状态管理 支持@State等 不支持
参数传递 @Prop/@Link 直接传参

三、动手实践

3.1 基础练习:通用按钮组件

创建一个可复用的按钮组件:

// 按钮类型枚举
enum 按钮类型 {
  主要 = "#2196F3",
  成功 = "#4CAF50",
  警告 = "#FF9800",
  危险 = "#F44336",
  默认 = "#9E9E9E"
}

@Component
struct 通用按钮 {
  @Prop 文字: string = "按钮"
  @Prop 类型: string = "主要"    // 主要、成功、警告、危险、默认
  @Prop 禁用: boolean = false
  @Prop 加载中: boolean = false
  点击回调: () => void = () => {}

  // 获取按钮颜色
  获取颜色(): string {
    switch (this.类型) {
      case "主要": return 按钮类型.主要
      case "成功": return 按钮类型.成功
      case "警告": return 按钮类型.警告
      case "危险": return 按钮类型.危险
      default: return 按钮类型.默认
    }
  }

  build() {
    Button(this.加载中 ? "加载中..." : this.文字, { type: ButtonType.Capsule })
      .width('100%')
      .height(48)
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .backgroundColor(this.获取颜色())
      .enabled(!this.禁用 && !this.加载中)
      .opacity(this.禁用 ? 0.5 : 1)
      .onClick(() => {
        if (!this.禁用 && !this.加载中) {
          this.点击回调()
        }
      })
  }
}

// 使用示例
@Entry
@Component
// 完整可运行代码,复制到 Index.ets 即可运行
struct Index {
  @State 加载状态: boolean = false

  build() {
    Column({ space: 15 }) {
      Text("通用按钮组件")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin(20)
      
      通用按钮({
        文字: "主要按钮",
        类型: "主要",
        点击回调: () => console.log("点击了主要按钮")
      })
      
      通用按钮({
        文字: "成功按钮",
        类型: "成功",
        点击回调: () => console.log("点击了成功按钮")
      })
      
      通用按钮({
        文字: "警告按钮",
        类型: "警告",
        点击回调: () => console.log("点击了警告按钮")
      })
      
      通用按钮({
        文字: "危险按钮",
        类型: "危险",
        点击回调: () => console.log("点击了危险按钮")
      })
      
      通用按钮({
        文字: "禁用按钮",
        类型: "主要",
        禁用: true
      })
      
      通用按钮({
        文字: "加载按钮",
        类型: "主要",
        加载中: this.加载状态,
        点击回调: () => {
          this.加载状态 = true
          setTimeout(() => this.加载状态 = false, 2000)
        }
      })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}

3.2 进阶练习:信息卡片组件

创建一个可复用的信息卡片组件:

// 定义卡片数据类型
interface 卡片数据 {
  标题: string
  内容: string
  图片?: string
  标签?: string
  时间?: string
}

@Component
struct 信息卡片 {
  @Prop 数据: 卡片数据
  点击回调: () => void = () => {}
  长按回调: () => void = () => {}

  build() {
    Column({ space: 10 }) {
      // 图片区域(如果有)
      if (this.数据.图片) {
        Stack({ alignContent: Alignment.TopEnd }) {
          Text(this.数据.图片)
            .fontSize(60)
            .width('100%')
            .height(150)
            .backgroundColor('#E3F2FD')
            .textAlign(TextAlign.Center)
          
          if (this.数据.标签) {
            Text(this.数据.标签)
              .fontSize(11)
              .fontColor('#FFFFFF')
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .backgroundColor('#FF5722')
              .borderRadius(4)
              .margin(8)
          }
        }
      }
      
      // 内容区域
      Column({ space: 8 }) {
        Text(this.数据.标题)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Text(this.数据.内容)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(3)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        if (this.数据.时间) {
          Text(this.数据.时间)
            .fontSize(12)
            .fontColor('#999999')
            .margin({ top: 5 })
        }
      }
      .width('100%')
      .padding(15)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 6, color: '#10000000' })
    .onClick(() => this.点击回调())
    .gesture(
      LongPressGesture({ duration: 500 })
        .onAction(() => this.长按回调())
    )
  }
}

// 使用示例
@Entry
// 完整可运行代码,复制到 Index.ets 即可运行
@Component
struct Index {
  @State 卡片列表: 卡片数据[] = [
    {
      标题: "ArkTS开发入门教程",
      内容: "本教程面向零基础学习者,通过通俗易懂的方式讲解ArkTS开发...",
      图片: "📚",
      标签: "热门",
      时间: "2024-01-15"
    },
    {
      标题: "鸿蒙生态介绍",
      内容: "HarmonyOS是华为开发的分布式操作系统,支持多种设备...",
      图片: "📱",
      时间: "2024-01-14"
    },
    {
      标题: "组件化开发技巧",
      内容: "学习如何创建可复用的组件,提高开发效率...",
      标签: "新课",
      时间: "2024-01-13"
    }
  ]

  build() {
    Column() {
      Text("信息卡片组件")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin(20)
      
      List({ space: 15 }) {
        ForEach(this.卡片列表, (卡片: 卡片数据, 索引: number) => {
          ListItem() {
            信息卡片({
              数据: 卡片,
              点击回调: () => {
                console.log("点击了卡片:" + 卡片.标题)
              },
              长按回调: () => {
                console.log("长按了卡片:" + 卡片.标题)
              }
            })
          }
        })
      }
      .padding(15)
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

3.3 挑战练习:弹窗组件

创建一个可复用的弹窗组件:

@Component
struct 确认弹窗 {
  @Prop 显示: boolean = false
  @Prop 标题: string = "提示"
  @Prop 内容: string = ""
  @Prop 确认文字: string = "确定"
  @Prop 取消文字: string = "取消"
  @Prop 显示取消: boolean = true
  
  确认回调: () => void = () => {}
  取消回调: () => void = () => {}

  build() {
    Stack() {
      // 遮罩层
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#80000000')
        .onClick(() => {
          if (this.显示取消) {
            this.取消回调()
          }
        })
      
      // 弹窗内容
      Column({ space: 20 }) {
        Text(this.标题)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        
        Text(this.内容)
          .fontSize(16)
          .fontColor('#666666')
          .textAlign(TextAlign.Center)
        
        Row({ space: 15 }) {
          if (this.显示取消) {
            Button(this.取消文字, { type: ButtonType.Capsule })
              .width(100)
              .height(40)
              .backgroundColor('#F5F5F5')
              .fontColor('#666666')
              .onClick(() => this.取消回调())
          }
          
          Button(this.确认文字, { type: ButtonType.Capsule })
            .width(100)
            .height(40)
            .backgroundColor('#2196F3')
            .onClick(() => this.确认回调())
        }
      }
      .width('80%')
      .padding(25)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .visibility(this.显示 ? Visibility.Visible : Visibility.Hidden)
  }
}

// 使用示例
// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
  @State 显示弹窗: boolean = false
  @State 弹窗标题: string = ""
  @State 弹窗内容: string = ""
  @State 操作结果: string = ""

  build() {
    Stack() {
      Column({ space: 20 }) {
        Text("弹窗组件演示")
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin(20)
        
        Button("删除确认弹窗")
          .onClick(() => {
            this.弹窗标题 = "确认删除"
            this.弹窗内容 = "确定要删除这条记录吗?删除后无法恢复。"
            this.显示弹窗 = true
          })
        
        Button("提示弹窗")
          .onClick(() => {
            this.弹窗标题 = "操作成功"
            this.弹窗内容 = "您的操作已成功完成!"
            this.显示弹窗 = true
          })
        
        if (this.操作结果 != "") {
          Text(`操作结果:${this.操作结果}`)
            .fontSize(16)
            .fontColor('#2196F3')
            .margin(20)
        }
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F5F5F5')
      
      // 弹窗组件
      确认弹窗({
        显示: this.显示弹窗,
        标题: this.弹窗标题,
        内容: this.弹窗内容,
        显示取消: this.弹窗标题 == "确认删除",
        确认回调: () => {
          this.操作结果 = "点击了确定"
          this.显示弹窗 = false
        },
        取消回调: () => {
          this.操作结果 = "点击了取消"
          this.显示弹窗 = false
        }
      })
    }
  }
}

四、知识总结

4.1 核心概念回顾

  1. 组件:界面的独立、可复用部分
  2. @Component:标记自定义组件
  3. @Prop:父组件向子组件传数据(单向)
  4. @Link:父子组件双向绑定
  5. 回调函数:子组件向父组件通信
  6. @Builder:轻量级构建器

4.2 组件通信方式

方式 方向 用途 语法
@Prop 父→子 传递数据 @Prop 属性: 类型
@Link 父↔子 双向同步 @Link 属性: 类型,传递用$属性
回调函数 子→父 事件通知 回调名: () => void = () => {}

4.3 组件设计原则

  1. 单一职责:一个组件只做一件事
  2. 可复用性:组件应该可以在多个地方使用
  3. 可配置性:通过@Prop让组件行为可定制
  4. 可测试性:组件应该可以独立测试

4.4 常见错误提醒

错误现象 原因 解决方法
@Prop不生效 没传值或类型不匹配 检查传递的参数
@Link报错 没用$符号传递 使用$属性名传递
回调不触发 没绑定回调函数 确保传入回调函数
组件不显示 组件名拼写错误 检查组件名称

五、课后作业

5.1 巩固练习(必做)

练习1:输入框组件

封装一个通用输入框组件:

  • 支持placeholder
  • 支持密码模式
  • 支持验证提示
  • 支持清除按钮

练习2:评分组件

封装一个五星评分组件:

  • 显示星星(支持半星)
  • 支持点击评分
  • 支持只读模式
  • 显示分数

练习3:标签组件

封装一个标签选择组件:

  • 显示多个标签
  • 支持单选/多选
  • 支持自定义颜色
  • 支持删除标签

5.2 创意编程(选做)

创意1:轮播图组件

  • 自动轮播
  • 支持手动滑动
  • 显示指示器
  • 支持循环播放

创意2:下拉刷新组件

  • 支持下拉刷新
  • 显示刷新动画
  • 支持上拉加载更多
  • 显示加载状态

创意3:步骤条组件

  • 显示多个步骤
  • 标记当前步骤
  • 支持点击跳转
  • 显示步骤状态

5.3 下篇预习

下一篇,我们将学习页面跳转,实现多页面应用。预习问题:

  1. 怎么从一个页面跳转到另一个页面?
  2. 怎么传递数据到下一个页面?
  3. 怎么返回上一个页面?

恭喜你完成了第8篇的学习! 🎉

现在你已经掌握了组件化开发,可以创建可复用的界面组件了。记住:组件化让代码更整洁,复用性让开发更高效

下节课,我们将学习如何实现页面跳转!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐