HarmonyOS实战:源码架构与首页联动解析

发布时间:2026/6/16 5:05:29

HarmonyOS实战:源码架构与首页联动解析 留痕 HarmonyOS 实战系列图1第二章封面先从首页串起拍照、录音、记录和统计。先把源码地图铺开第一篇已经把工程跑起来了这一篇开始进入源码导览。对于留痕这个项目来说真正值得先看的不是某一个组件而是首页、服务层、路由和数据模型如何连成一条线。只要把这条线看清楚后面拍照、录音、水印、记录、统计五条功能支路就都好理解了。本章的阅读目标很明确先知道哪些文件负责什么再看首页怎么刷新最后确认页面切换和数据重算是怎样把整个应用串起来的。源码地图文件/模块职责这一章会怎么用entry/src/main/ets/pages/Index.ets首页工作台和 Tab 切换看它怎么刷新概览、功能入口和当前页dynamiclibrary/src/main/ets/services/WorkClockService.ets业务服务、状态持久化和统计汇总看它怎么提供首页数据和记录数据staticlibrary/src/main/ets/models/WorkClockModels.etsWorkRecord、WatermarkTemplate 等共享模型看数据字段是如何统一的entry/src/main/ets/common/Routes.ets页面路由和功能入口映射看首页按钮如何跳到各功能页staticlibrary/src/main/ets/common/AppTabs.ets底部 Tab 常量看首页、记录、相机、统计、我的如何切换entry/src/main/ets/entryability/EntryAbility.ets入口初始化和主页加载看应用启动后首页怎么出现图2应用源码架构图UI、Service、Model 和 Route 四层关系一眼就能看清。首页为什么能自己刷新首页的关键不是静态页面而是它和服务层之间的联动。Index.ets 会在页面显示时读取本地的 Tab 状态和服务层数据只要记录或模板有变化页面就会随着版本号刷新。这样首页、记录页和统计页看到的就是同一份数据。StorageLink(AppStorageKeys.WORKCLOCK_VERSION)Watch(handleWorkClockVersionChanged)private workclockVersion: number 0;private refreshAllData(resetCalendarDate: boolean): void {const storedTab: string | undefined AppStorage.get(AppStorageKeys.MAIN_TAB) as string | undefined;if (storedTab storedTab.length 0) {this.currentTab storedTab;}this.overview this.service.getOverviewSnapshot();this.actions this.service.getHomeActions();this.records this.service.getRecords();this.recentRecords this.service.getRecentRecords();this.featureBullets this.service.getFeatureBullets();this.settings this.service.getSettings();}刷新来源读取内容页面结果AppStorageKeys.MAIN_TAB上次选中的 Tab页面恢复到用户离开时的模块WORKCLOCK_VERSION服务层版本号数据变化后首页重新读取getOverviewSnapshot()今日记录、本月记录、项目数等概览首页头部摘要同步更新getRecords()/getRecentRecords()全部记录和最近记录记录卡片和最近记录区刷新底部 Tab 是怎么切页的底部 Tab 看起来只是五个按钮实际上它把整个应用的主导航固定了下来。AppTabs 给出五个稳定的 tab idIndex 再根据当前 tab 决定要渲染哪个页面。export class AppTabs {static readonly HOME: string home;static readonly RECORDS: string records;static readonly CAMERA: string camera;static readonly STATS: string stats;static readonly PROFILE: string profile;}Builderprivate buildCurrentTab() {if (this.currentTab AppTabs.RECORDS) {this.buildCalendarTab()} else if (this.currentTab AppTabs.CAMERA) {this.buildCameraPreviewTab()} else if (this.currentTab AppTabs.STATS) {this.buildStatsTab()} else if (this.currentTab AppTabs.PROFILE) {this.buildProfileTab()} else {this.buildHomeTab()}}图3路由地图首页和各功能页之间的关系更容易理解。路由表把页面串起来Routes.ets 负责把首页按钮和设置入口映射到具体页面。这样做的好处是页面名集中管理后续改路由时不用在每个按钮里到处找字符串。export class Routes {static readonly MAIN_PAGE: string pages/Index;static readonly FEATURE_CAPTURE: string pages/FeatureCapturePage;static readonly FEATURE_VOICE: string pages/FeatureVoicePage;static readonly FEATURE_WATERMARK: string pages/FeatureWatermarkPage;static readonly FEATURE_PROJECT: string pages/FeatureProjectPage;static readonly FEATURE_RECORD: string pages/FeatureRecordPage;static readonly FEATURE_CALENDAR: string pages/FeatureCalendarPage;static readonly FEATURE_STATS: string pages/FeatureStatsPage;static readonly FEATURE_SETTINGS: string pages/FeatureSettingsPage;static resolveFeatureRoute(featureId: string): string | undefined {switch (featureId) {case capture: return Routes.FEATURE_CAPTURE;case voice: return Routes.FEATURE_VOICE;case watermark: return Routes.FEATURE_WATERMARK;case project: return Routes.FEATURE_PROJECT;case record: return Routes.FEATURE_RECORD;case calendar: return Routes.FEATURE_CALENDAR;case stats: return Routes.FEATURE_STATS;case settings: return Routes.FEATURE_SETTINGS;default: return undefined;}}}入口 id对应页面典型场景captureFeatureCapturePage现场拍照voiceFeatureVoicePage现场录音watermarkFeatureWatermarkPage水印模板projectFeatureProjectPage项目管理recordFeatureRecordPage记录管理calendarFeatureCalendarPage日历回看statsFeatureStatsPage统计分析settingsFeatureSettingsPage设置中心服务层负责什么WorkClockService 是整个应用的数据中枢。它负责启动时初始化本地仓库、读取持久化状态、构造首页概览、提供最近记录、管理水印模板、保存分类和备注选项以及在记录变化时重新发布版本号。bootstrap(context: common.UIAbilityContext): void {AppStorage.setOrCreatenumber(AppStorageKeys.WORKCLOCK_VERSION, 0);AppStorage.setOrCreatestring(AppStorageKeys.MAIN_TAB, AppTabs.HOME);WorkClockRepository.initialize(context);const raw: string WorkClockRepository.readState();if (raw.length 0) {const state: WorkClockState this.parseState(raw);this.records state.records;this.watermarkTemplates state.watermarkTemplates ?? WorkClockService.buildDefaultWatermarkTemplates();this.selectedWatermarkTemplateId state.selectedWatermarkTemplateId ?? this.watermarkTemplates[0].id;this.categoryOptions state.categoryOptions ?? WorkClockService.buildDefaultCategoryOptions();this.noteOptions state.noteOptions ?? WorkClockService.buildDefaultNoteOptions();this.publishSelectedWatermarkSnapshot();this.bumpVersion();return;}this.publishSelectedWatermarkSnapshot();this.persist();}getOverviewSnapshot(): OverviewSnapshot {let totalMinutes: number 0;const projectMap: Recordstring, boolean {};this.records.forEach((record: WorkRecord) {totalMinutes record.durationMinutes;projectMap[record.projectName] true;});const latestRecord: WorkRecord | undefined this.records[0];return {todayPunchCount: this.getSelectedDayRecords().length,monthlyRecordCount: this.records.length,projectCount: Object.keys(projectMap).length,totalMinutes: totalMinutes,latestProject: latestRecord?.projectName ?? --,latestLocation: latestRecord?.location ?? --};}private persist(): void {WorkClockRepository.writeState(JSON.stringify({records: this.records,watermarkTemplates: this.watermarkTemplates,selectedWatermarkTemplateId: this.selectedWatermarkTemplateId,categoryOptions: this.categoryOptions,noteOptions: this.noteOptions} as WorkClockState));this.publishSelectedWatermarkSnapshot();this.bumpVersion();}数据模型长什么样UI 层和 Service 层之所以能互相理解是因为它们共用一套模型定义。WorkRecord 承载照片和录音记录WatermarkTemplate 承载水印样式StatsSummary 承载统计图表的数据。模型核心字段用途WorkRecordprojectName / captureDate / location / note / mediaUri / audioUri承载照片、录音和备注WatermarkTemplatetitle / accentColor / backgroundColor / timeLabel / locationLabel承载水印模板OverviewSnapshottodayPunchCount / monthlyRecordCount / projectCount / totalMinutes承载首页概览StatsSummarymetrics / trend / categories承载统计页图表CalendarDayday / isSelected / hasRecord承载日历回看export interface WorkRecord {id: string;title: string;projectName: string;captureDate: string;captureTime: string;durationLabel: string;durationMinutes: number;location: string;weather: string;temperature: string;note: string;category: string;mediaUri?: string;audioUri?: string;}读源码时建议的顺序先看 Routes.ets 和 AppTabs.ets把页面与入口的名字对上。再看 Index.ets 的 refreshAllData() 和 buildCurrentTab()理解首页怎样决定自己展示什么。接着看 WorkClockService 的 bootstrap()、getOverviewSnapshot() 和 persist()理解数据如何读写。最后回到 WorkClockModels.ets确认页面和服务共用的字段到底有哪些。四、首页、路由和服务层的连接点第二篇把源码地图铺开之后最值得再单独讲清楚的是首页、路由和服务层之间的连接点。首页不是孤立页面Index.ets 负责把服务层快照重新渲染出来AppTabs 负责保存底部栏状态Routes 负责把功能入口映射到具体页面WorkClockService 则负责提供真实数据。四者配在一起整个项目才会看起来像一条完整链路。模块职责这一章要盯什么Index.ets首页状态和刷新看 refreshAllData() 如何同步概览、记录和设置Routes.ets功能入口映射看 featureId 如何落到实际页面名AppTabs.ets底部 Tab 常量看首页如何记住用户上次停留的位置WorkClockService.ets业务数据中枢看概览、记录和统计如何共用同一份数据private refreshAllData(resetCalendarDate: boolean): void {const storedTab: string | undefined AppStorage.get(AppStorageKeys.MAIN_TAB) as string | undefined;if (storedTab storedTab.length 0) {this.currentTab storedTab;}this.overview this.service.getOverviewSnapshot();this.actions this.service.getHomeActions();this.records this.service.getRecords();this.recentRecords this.service.getRecentRecords();}如果只看某一个页面很容易觉得留痕只是“拍照、录音、记录、统计”四五个功能块但从源码角度看它更像是一个共享数据中枢。首页把入口和概览聚合起来路由把动作分发出去服务层把数据收回来这样后面每个功能页的回流路径才会稳定。本篇小结第二篇真正做的事情是把留痕这个项目的源码地图画出来。只要你知道首页怎么刷新、路由怎么跳转、服务层怎么持久化、模型怎么承载数据后面再看拍照、录音、水印、记录和统计就不会再觉得每个页面都是孤立的。下一篇会继续看入口 Ability 和首页工作台看看应用是怎样从启动阶段进入首页并把拍照、录音和记录入口摆到用户面前的。今日作业打开 Index.ets找到 refreshAllData() 和 buildCurrentTab()在脑子里画出首页刷新链路。打开 WorkClockService.ets找出 bootstrap()、getOverviewSnapshot() 和 persist() 的关系。打开 Routes.ets把 featureId 和实际页面名对应起来。本章导读这一章开始从源码地图看项目。我们不只看“能用”还要看首页、拍照、录音、记录和统计之间是怎么互相接上的。先看首页承接几个入口。再看拍照和录音如何回到记录页。最后看统计页如何从同一批数据重算结果。把入口和回流关系看清楚后面再看每一页的细节就不会散。打开 WorkClockModels.ets确认 WorkRecord 的字段能否覆盖照片和录音两条记录。

相关新闻