HarmonyOS APP<玩转React>开源教程十:组件化开发概述
第10次:HeroBanner 组件开发
组件化是现代 UI 开发的核心思想。本次课程将把首页的 HeroBanner 抽取为独立的可复用组件,学习组件设计、Props 定义、渐变背景实现等关键技术。
学习目标
- 理解组件化开发的意义与优势
- 掌握组件 Props 设计原则
- 学会使用 @Prop 装饰器传递数据
- 实现渐变背景效果
- 完成 HeroBanner 组件的完整开发
10.1 组件化开发概述
为什么要组件化?
在第9次课程中,我们在 Index.ets 中直接编写了 HeroBanner 的代码。随着应用功能增加,这种方式会导致:
- 代码臃肿:单个文件代码量过大,难以维护
- 复用困难:相同 UI 无法在其他页面使用
- 职责不清:页面逻辑与组件逻辑混杂
组件化开发的优势:
| 优势 | 说明 |
|---|---|
| 可复用性 | 同一组件可在多处使用 |
| 可维护性 | 修改组件只需改一处 |
| 可测试性 | 组件可独立测试 |
| 协作效率 | 团队成员可并行开发 |
组件设计原则
- 单一职责:一个组件只做一件事
- 高内聚低耦合:组件内部紧密,对外依赖少
- Props 向下传递:父组件通过 Props 控制子组件
- 事件向上传递:子组件通过回调通知父组件
10.2 组件设计与 Props 定义
分析 HeroBanner 需求
HeroBanner 需要展示:
- 欢迎语和副标题
- 学习统计(已完成、总课程、连续天数)
- 每日一题入口
需要从外部接收的数据:
completedLessons:已完成课程数totalLessons:总课程数streak:连续学习天数onDailyQuestionTap:每日一题点击回调
Props 定义
@Component
export struct HeroBanner {
// 使用 @Prop 接收父组件传递的数据
@Prop completedLessons: number = 0;
@Prop totalLessons: number = 0;
@Prop streak: number = 0;
// 访问全局状态
@StorageLink('isDarkMode') isDarkMode: boolean = false;
// 回调函数(可选)
onDailyQuestionTap?: () => void;
}
@Prop vs @Link
| 装饰器 | 数据流向 | 使用场景 |
|---|---|---|
| @Prop | 单向(父→子) | 子组件只读数据 |
| @Link | 双向 | 子组件需要修改数据 |
HeroBanner 只需要展示数据,不需要修改,所以使用 @Prop。
10.3 渐变背景实现
linearGradient 属性
ArkUI 提供 linearGradient 属性实现线性渐变:
Column()
.linearGradient({
angle: 135, // 渐变角度
colors: [ // 颜色数组
['#61DAFB', 0], // [颜色, 位置]
['#21a0c4', 1]
]
})
渐变角度说明
0°
↑
315° ←┼→ 45°
↓
180°
0°:从下到上90°:从左到右135°:从左上到右下180°:从上到下
实现渐变背景
@Builder
GradientBackground() {
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 135,
colors: [['#61DAFB', 0], ['#21a0c4', 1]]
})
}
使用 Stack 叠加内容
Stack() {
// 底层:渐变背景
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 135,
colors: [['#61DAFB', 0], ['#21a0c4', 1]]
})
// 上层:内容
Column() {
Text('内容')
.fontColor('#ffffff')
}
.padding(20)
}
10.4 学习统计展示
统计区域布局
使用 Row 布局,三个统计项平均分配空间:
Row() {
// 已完成
Column() {
Text(`${this.completedLessons}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('已完成')
.fontSize(12)
.fontColor('rgba(255,255,255,0.95)')
}
.layoutWeight(1)
// 分隔线
Column()
.width(1)
.height(40)
.backgroundColor('rgba(255,255,255,0.3)')
// 总课程
Column() {
Text(`${this.totalLessons}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('总课程')
.fontSize(12)
.fontColor('rgba(255,255,255,0.95)')
}
.layoutWeight(1)
// 分隔线
Column()
.width(1)
.height(40)
.backgroundColor('rgba(255,255,255,0.3)')
// 连续天数
Column() {
Text(`${this.streak}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('连续天数')
.fontSize(12)
.fontColor('rgba(255,255,255,0.95)')
}
.layoutWeight(1)
}
.width('100%')
.margin({ top: 20 })
.padding({ left: 16, right: 16 })
layoutWeight 说明
layoutWeight 用于在 Row/Column 中按比例分配剩余空间:
Row() {
Column().layoutWeight(1) // 占 1/3
Column().layoutWeight(1) // 占 1/3
Column().layoutWeight(1) // 占 1/3
}
10.5 每日一题入口
入口设计
Row() {
Text('📝 每日一题')
.fontSize(14)
.fontColor('#ffffff')
Blank() // 占据中间空间
Text('挑战 →')
.fontSize(14)
.fontColor('rgba(255,255,255,0.95)')
}
.width('100%')
.padding(12)
.margin({ top: 16 })
.backgroundColor('rgba(255,255,255,0.15)') // 半透明白色背景
.borderRadius(12)
.onClick(() => {
if (this.onDailyQuestionTap) {
this.onDailyQuestionTap();
}
})
回调函数处理
组件通过可选的回调函数与父组件通信:
// 组件定义
onDailyQuestionTap?: () => void;
// 点击时调用
.onClick(() => {
if (this.onDailyQuestionTap) {
this.onDailyQuestionTap();
}
})
// 父组件使用
HeroBanner({
completedLessons: 5,
totalLessons: 35,
streak: 3,
onDailyQuestionTap: () => {
router.pushUrl({ url: 'pages/QuizPage' });
}
})
10.6 实操:完成 HeroBanner.ets 组件
步骤一:创建组件文件
在 entry/src/main/ets/components/ 目录下创建 HeroBanner.ets 文件。
步骤二:编写完整组件代码
/**
* 首页横幅组件
* 显示学习统计和每日一题入口
*/
@Component
export struct HeroBanner {
// Props:从父组件接收的数据
@Prop completedLessons: number = 0;
@Prop totalLessons: number = 0;
@Prop streak: number = 0;
// 全局状态:主题模式
@StorageLink('isDarkMode') isDarkMode: boolean = false;
// 回调函数:每日一题点击事件
onDailyQuestionTap?: () => void;
build() {
Column() {
// 使用 Stack 叠加渐变背景和内容
Stack() {
// 渐变背景层
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 135,
colors: [['#61DAFB', 0], ['#21a0c4', 1]]
})
// 内容层
Column() {
// 欢迎语
Text('⚛️ React 学习之旅')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('由浅入深,系统掌握 React')
.fontSize(14)
.fontColor('rgba(255,255,255,0.95)')
.margin({ top: 4 })
// 学习统计卡片
Row() {
// 已完成课程
Column() {
Text(`${this.completedLessons}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('已完成')
.fontSize(12)
.fontColor('rgba(255,255,255,0.95)')
}
.layoutWeight(1)
// 分隔线
Column()
.width(1)
.height(40)
.backgroundColor('rgba(255,255,255,0.3)')
// 总课程
Column() {
Text(`${this.totalLessons}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('总课程')
.fontSize(12)
.fontColor('rgba(255,255,255,0.95)')
}
.layoutWeight(1)
// 分隔线
Column()
.width(1)
.height(40)
.backgroundColor('rgba(255,255,255,0.3)')
// 连续天数
Column() {
Text(`${this.streak}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('连续天数')
.fontSize(12)
.fontColor('rgba(255,255,255,0.95)')
}
.layoutWeight(1)
}
.width('100%')
.margin({ top: 20 })
.padding({ left: 16, right: 16 })
// 每日一题入口
Row() {
Text('📝 每日一题')
.fontSize(14)
.fontColor('#ffffff')
Blank()
Text('挑战 →')
.fontSize(14)
.fontColor('rgba(255,255,255,0.95)')
}
.width('100%')
.padding(12)
.margin({ top: 16 })
.backgroundColor('rgba(255,255,255,0.15)')
.borderRadius(12)
.onClick(() => {
if (this.onDailyQuestionTap) {
this.onDailyQuestionTap();
}
})
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.height(220)
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.clip(true) // 裁剪超出圆角的内容
}
.width('100%')
}
}
步骤三:在首页中使用组件
更新 Index.ets,导入并使用 HeroBanner 组件:
// 导入组件
import { HeroBanner } from '../components/HeroBanner';
@Entry
@Component
struct Index {
@State currentTab: number = 0;
@State completedCount: number = 0;
@State totalCount: number = 35;
@State streakDays: number = 0;
@StorageLink('isDarkMode') isDarkMode: boolean = false;
build() {
Column() {
Tabs({ barPosition: BarPosition.End, index: this.currentTab }) {
TabContent() {
this.HomeContent()
}
.tabBar(this.TabBuilder('🏠', '首页', 0))
// ... 其他 Tab
}
}
}
@Builder
HomeContent() {
Scroll() {
Column() {
// 使用 HeroBanner 组件
HeroBanner({
completedLessons: this.completedCount,
totalLessons: this.totalCount,
streak: this.streakDays,
onDailyQuestionTap: () => {
// 跳转到每日一题页面
router.pushUrl({ url: 'pages/QuizPage' });
}
})
// 其他内容...
}
}
}
}
步骤四:验证组件效果
- 运行应用,查看首页 HeroBanner 显示效果
- 检查渐变背景是否正确显示
- 点击"每日一题"入口,验证回调是否触发
- 修改传入的数据,验证组件是否正确更新
组件代码解析
关键点总结
- @Component + export:使组件可被其他文件导入
@Component
export struct HeroBanner {
// ...
}
- @Prop 默认值:为 Props 提供默认值,避免未传参时报错
@Prop completedLessons: number = 0;
- 可选回调:使用
?标记回调为可选
onDailyQuestionTap?: () => void;
- 安全调用:调用回调前检查是否存在
if (this.onDailyQuestionTap) {
this.onDailyQuestionTap();
}
- clip(true):配合 borderRadius 裁剪内容
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.clip(true)
本次课程小结
通过本次课程,你已经:
✅ 理解了组件化开发的意义与优势
✅ 掌握了组件 Props 设计原则
✅ 学会了使用 @Prop 装饰器传递数据
✅ 实现了渐变背景效果
✅ 完成了 HeroBanner 组件的完整开发
课后练习
-
添加动画效果:为统计数字添加数字滚动动画
-
主题适配:根据 isDarkMode 调整渐变颜色
-
扩展 Props:添加自定义标题和副标题的 Props
下次预告
第11次:ModuleCard 模块卡片组件
我们将开发另一个核心组件 ModuleCard:
- 卡片组件设计
- 难度等级标签
- 进度条展示
- 锁定状态处理
继续深入组件化开发!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)