HarmonyOS APP<玩转React>开源教程十五:首页完整实现
第15次:首页完整实现
经过前面的学习,我们已经开发了 HeroBanner、ModuleCard 组件以及 TutorialService、ProgressService 服务。本次课程将整合所有内容,完成首页的完整实现。
首页效果

第15次:首页完整实现
本文讲解首页主框架的完整实现。这个页面不只是一个 Banner 入口,而是整个应用的控制中心:既负责初始化服务,又承载首页、课程、源码、项目、个人中心五个 Tab 的内容。
本篇先看懂什么
首页是最容易“越写越乱”的页面,因为它既有数据初始化,又有多区块组合,还要负责页面切换后的刷新。Index.ets 实际承担了下面这些职责:
- 初始化主题、存储、教程数据、搜索服务、徽章服务、收藏服务。
- 维护底部 Tab 切换。
- 组织 HeroBanner、快捷入口、继续学习、推荐模块等首页区块。
- 同时承载课程、源码学习、开源项目、个人中心几个内容面板。
- 处理页面回显时的进度刷新。
- 附带一个节日烟花动画层。
如果你先把这张结构图看明白,后面的代码阅读会轻松很多。
一、先明确:首页不是“单页面”,而是一个容器页
很多初学者看到 Index.ets 会下意识认为它只是首页,但实际这个文件已经把应用里最重要的几个主视图都合并在了一起。也就是说,Index 更像一个“壳层页面”:
- 底部
Tabs负责大区域切换。 - 每个 Tab 对应一个
@Builder。 - 子内容通过服务层获取数据,不在页面里硬编码。
这种写法的好处是切换底部 Tab 时状态比较集中,课程、进度、徽章这些数据都能在一个地方维护。缺点是文件会变长,所以你在阅读时要养成一个习惯:先从状态定义和生命周期看起,再跳到某个具体 Builder,千万不要一上来就从第一行一路读到最后。
二、真实初始化流程应该怎么理解
首页初始化的顺序如下:
aboutToAppear()先调用initTheme(getContext(this))。- 然后执行
initAndLoadData()。 initAndLoadData()内部先初始化本地存储。- 再初始化
TutorialService,拿到全部模块数据。 - 用模块数据初始化
SearchService和BadgeService。 - 读取
ProgressService里的学习进度。 - 加载收藏数据。
- 再根据当前进度检查是否需要发放徽章。
- 最后初始化源码学习和开源项目两个数据面板。
这套顺序不是随便排的。比如 BadgeService 依赖模块列表,SearchService 也依赖模块列表,如果你把它们放在 TutorialService.init() 之前,就会出现服务拿不到完整数据的问题。
把这段讲解对照到真实源码里看,会一下清楚很多:
private async initAndLoadData(): Promise<void> {
try {
await StorageUtil.init(getContext(this));
TutorialService.init();
this.modules = TutorialService.getAllModules();
SearchService.init(this.modules);
BadgeService.init(this.modules);
this.progress = await ProgressService.loadProgress();
await BookmarkService.loadBookmarks();
await BadgeService.checkAndAwardBadges(this.progress);
this.progress = await ProgressService.loadProgress();
this.sourceCategories = SourceCodeData.getCategories();
if (this.sourceCategories.length > 0) {
this.selectedSourceCategory = this.sourceCategories[0].id;
this.sourceChapters = SourceCodeData.getChaptersByCategory(this.selectedSourceCategory);
}
this.projectCategories = OpenSourceProjectData.getCategories();
if (this.projectCategories.length > 0) {
this.selectedProjectCategory = this.projectCategories[0].id;
this.projects = OpenSourceProjectData.getProjectsByCategory(this.selectedProjectCategory);
}
} catch (error) {
console.error('[Index] Failed to load data:', error);
} finally {
this.isLoading = false;
}
}
三、首页区块是怎么拼出来的
真正的首页内容集中在 HomeContent() 中,它不是一个复杂算法,而是一个非常清晰的内容编排:
- 顶部先放
HeroBanner - 然后是
QuickAccessSection - 如果存在
currentLesson,再显示ContinueLearningSection - 最后放
RecommendedModulesSection
这说明首页设计遵循的是“先总览、再入口、再个性化、最后推荐”的顺序。这个顺序很适合学习类应用,因为用户打开应用最关心的是:
- 我学到哪了。
- 我现在能做什么。
- 有没有推荐内容。
ContinueLearningSection() 也不是凭空生成标题,而是直接读取 this.progress.currentLesson,再通过 findModuleIdByLessonId() 找回所属模块,最后跳转到 LessonDetail。这也是为什么上一节我们要强调进度服务里 currentLesson 字段的重要性。
对应到 HomeContent(),正文顺序就是这样一层层排下来的:
@Builder
HomeContent() {
Scroll() {
Column() {
HeroBanner({
completedLessons: this.progress.completedLessons.length,
totalLessons: this.getTotalContentCount(),
streak: this.progress.learningStreak,
onDailyQuestionTap: () => {
router.pushUrl({ url: 'pages/QuizPage' });
}
})
this.QuickAccessSection()
if (this.progress.currentLesson) {
this.ContinueLearningSection()
}
this.RecommendedModulesSection()
}
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
}
四、底部 Tab 的实现思路值得你照着学
当前项目使用 Tabs + TabContent + 自定义 tabBar Builder 的方式来实现底部导航。这样做的优势是:
- 图标和文字状态可以自己控制。
- 明暗主题切换时颜色可以统一处理。
- 后续要加徽章角标、动态样式时扩展更方便。
TabBuilder(title, icon, index) 这个方法虽然短,但作用很大。因为它把“当前是否选中”的判断统一收口了:
- 当前 Tab:使用亮色主题色。
- 非当前 Tab:使用灰色弱化。
你以后在鸿蒙里做底部导航时,不一定每次都要依赖复杂组件库。像这样用 Builder 自己拼,反而更适合项目化开发,样式和业务状态都抓得更牢。
五、课程、源码、项目、个人中心为什么也放在这个文件里
从工程拆分角度看,这个文件确实比较长,但它也有一个好处:你可以非常直观地看到整个应用主框架。
1. CourseContent()
这里按难度把所有模块分组展示。难度顺序是固定的:beginner、basic、intermediate、advanced、ecosystem。每个模块项点击后跳转到 ModuleDetail。
2. SourceCodeContent()
这一块不是跳到新页面,而是在首页内直接展示源码学习分类、章节列表、展开详情,再通过按钮进入 SourceCodeDetailPage。
3. OpenSourceContent()
结构和源码学习类似,只是数据源换成 OpenSourceProjectData,内容换成项目特性、安装方式、快速开始和相关链接。
4. ProfileContent()
个人中心展示学习统计、徽章列表和若干功能入口,比如收藏、搜索、每日一题、面试题库。它本质上就是“进度数据的消费页”。
所以你可以把 Index.ets 理解成一个主舞台:不同数据服务在后台提供数据,不同 Builder 负责前台渲染。
六、读这一页时要特别看清五个主 Tab
真正把首页、课程、源码、项目、我的串起来的,不是五个分散页面,而是 build() 里的五个 TabContent。读这一段时,你要把它当作应用主框架来看,而不是把它误以为只是一个普通首页。
TabContent() {
this.HomeContent()
}
.tabBar(this.TabBuilder('首页', '🏠', 0))
TabContent() {
this.CourseContent()
}
.tabBar(this.TabBuilder('课程', '📚', 1))
TabContent() {
this.SourceCodeContent()
}
.tabBar(this.TabBuilder('源码', '📖', 2))
TabContent() {
this.OpenSourceContent()
}
.tabBar(this.TabBuilder('项目', '🌟', 3))
TabContent() {
this.ProfileContent()
}
.tabBar(this.TabBuilder('我的', '👤', 4))
这一段代码至少说明了三件事:
- 五个主功能共用同一个页面壳层。
- 每个区域都有独立 Builder,结构清楚。
- 源码学习、开源项目、个人中心都和首页共享初始化后的服务数据。
继续往下读时,再留意两个容易忽略的点:
onPageShow()会在页面回显后刷新进度,保证从详情页返回时首页统计是新的。- 烟花动画状态放在页面内部管理,不会污染全局服务层。
七、你可以怎样自己动手验证这一页
建议你不要一次看完 1000 多行 Index.ets,而是按下面顺序操作:
- 先只看顶部状态字段,搞清楚它管理了哪些 Tab、哪些服务数据、哪些动画状态。
- 再看
aboutToAppear()和initAndLoadData(),把初始化顺序记下来。 - 只看
HomeContent(),理解首页区块怎么拼。 - 再看
CourseContent(),理解模块列表如何按难度生成。 - 最后分别看
SourceCodeContent()、OpenSourceContent()、ProfileContent()。
如果你这样分段阅读,会明显比“硬啃一个大文件”轻松很多。
八、写首页时最容易踩的坑
1. 在页面里乱写初始化顺序
服务之间有依赖关系,必须先初始化存储,再初始化教程数据,再初始化依赖模块数据的服务。
2. 页面返回后不刷新进度
如果缺少 onPageShow() 里的 refreshProgress(),那你在课程详情页完成学习后,首页统计不会及时变化。
3. 把所有 UI 都写成一个超大 build()
当前项目虽然文件长,但它至少把内容拆到了多个 Builder 中。你要延续这种拆法,而不是把所有 Row、Column 堆在一起。
4. 只关注首页,忽略它还是整个应用的容器
Index 不只是“首页展示页”,它还是整个主框架的状态中心。这一点如果理解错了,后面做扩展就会非常吃力。
本篇小结
真实项目里的首页,核心不在“放几个漂亮卡片”,而在于它要承担应用主框架的职责。你这次应该重点记住三件事:
Index.ets是容器页,不只是首页。- 初始化顺序要严格按依赖关系来。
- 首页、课程、源码、项目、个人中心其实共享同一套主状态和服务数据。
理解这一点之后,下一篇我们再单独拆开看课程列表区块,你就会发现首页里那部分代码其实没那么吓人。
课后练习
- 在不改动服务层的前提下,自己画一张
Index.ets的组件结构图。 - 观察
HomeContent()和ProfileContent()都读取了哪些progress字段,整理成表格。 - 思考为什么
Index.ets里的烟花动画状态不适合放进全局服务,而更适合放在页面内部状态中。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)