这一章只看一件事应用为什么能先把服务层准备好再把首页稳稳地加载出来。图 4-1 章首页封面Stage 模型入口与首页加载链路维度内容本章主题EntryAbility 如何在 Stage 模型下把“留痕”带到首页核心源码EntryAbility.ets、main_pages.json、module.json5、WorkClockService.ets、Index.ets阅读目标看懂启动链路、配置职责和首页数据为什么能在启动后直接出现先给结论在 Stage 模型里真正的入口不是首页页面本身而是 EntryAbility。EntryAbility 负责先 bootstrap再 loadContent首页只是最后被加载出来的那一页。本章导读这一章只看入口链路EntryAbility 怎么把首页送到用户面前main_pages.json 和 module.json5 又各自负责什么。看启动顺序怎么走。看首页为什么能一打开就有数据。看哪些配置决定首屏和路由。入口链路理顺之后再看首页的数据渲染就会自然很多。一、Stage 模型下真正的入口是 EntryAbility在这个项目里应用从桌面图标被点开之后最先被系统调用的是 EntryAbility。它不是一个普通页面而是负责把应用从“启动态”切换到“可见态”的第一道闸门。这个阶段如果没有把服务层和窗口链路准备好首页就算能渲染出来也只会是一张空壳。import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from kit.AbilityKit;import { hilog } from kit.PerformanceAnalysisKit;import { window } from kit.ArkUI;import { WorkClockService } from dynamiclibrary;import { Routes } from ../common/Routes;export default class EntryAbility extends UIAbility {onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {try {this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);WorkClockService.getInstance().bootstrap(this.context);} catch (err) {hilog.error(DOMAIN, testTag, Failed to set colorMode. Cause: %{public}s, JSON.stringify(err));}}onWindowStageCreate(windowStage: window.WindowStage): void {windowStage.loadContent(Routes.MAIN_PAGE, (err) {if (err.code) {hilog.error(DOMAIN, testTag, Failed to load the content. Cause: %{public}s, JSON.stringify(err));return;}hilog.info(DOMAIN, testTag, Succeeded in loading the content.);});}}阶段关键动作效果onCreate先切换颜色模式再调用 WorkClockService.bootstrap()服务层在首页出现之前完成准备onWindowStageCreate通过 Routes.MAIN_PAGE 加载首页入口和页面路由保持统一loadContent 失败记录 hilog 日志便于定位白屏或路由错误图 4-2 启动链路系统启动 - EntryAbility - bootstrap - 首页二、先 bootstrap再 loadContentEntryAbility.onCreate 里最关键的动作不是页面渲染而是 WorkClockService.bootstrap。它把本地持久化数据、默认模板和 AppStorage 状态先准备好再把首页交给 windowStage.loadContent。这样做的好处很直接首页第一次进入时拿到的就是可用数据而不是临时拼出来的默认壳。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();}步骤做什么结果初始化 AppStorage写入 WORKCLOCK_VERSION 和 MAIN_TAB首页和服务层有统一的轻量状态入口读取持久化状态从 WorkClockRepository 取回 JSON已有记录和模板可以直接恢复回填默认值当本地没有数据时装入默认模板和分类首次安装也不会空白发布水印快照把当前模板写回 AppStorage水印页和拍照页能同步读取这一段的关键bootstrap 不是“初始化一下就完了”而是把首页需要的默认状态、模板快照和持久化入口一次性搭好。首页之所以能直接展示内容是因为这一步已经先把底子铺好了。三、main_pages.json 和 module.json5 各管一半页面清单和 Ability 配置是两层职责。main_pages.json 只告诉系统哪些页面可以被加载module.json5 则负责声明入口 Ability、权限和启动能力。两份文件各管一半缺一块都不行。{src: [pages/Index,pages/FeatureCapturePage,pages/FeatureVoicePage,pages/FeatureWatermarkPage,pages/FeatureProjectPage,pages/FeatureRecordPage,pages/FeatureCalendarPage,pages/FeatureStatsPage,pages/FeatureSettingsPage]}{module: {name: entry,type: entry,mainElement: EntryAbility,pages: $profile:main_pages,requestPermissions: [{ name: ohos.permission.CAMERA, reason: $string:camera_permission_reason },{ name: ohos.permission.MICROPHONE, reason: $string:microphone_permission_reason },{ name: ohos.permission.APPROXIMATELY_LOCATION, reason: $string:location_permission_reason },{ name: ohos.permission.LOCATION, reason: $string:location_permission_reason }]}}图 4-3 配置关系main_pages.json、module.json5 与 EntryAbility 的分工配置项作用本章关注点mainElement指向入口 Ability系统先找到 EntryAbility再决定加载什么页面pages绑定页面清单首页和功能页都从这里被注册requestPermissions声明运行权限相机、麦克风和定位都在这里提前声明skills / actions支持桌面入口保证桌面点击能够进入应用四、首页为什么一打开就有数据首页不是自己去拉一份孤立的数据而是通过 AppStorage 里的轻量状态和服务层的快照来渲染。WorkClockService 负责维护真实数据Index 负责在页面生命周期里把这些数据读出来再渲染成首页、记录和统计卡片。export class AppStorageKeys {static readonly MAIN_TAB: string main.tab;static readonly WORKCLOCK_VERSION: string workclock.version;static readonly WATERMARK_TEMPLATE_ID: string watermark.templateId;static readonly WATERMARK_CATEGORY: string watermark.category;static readonly WATERMARK_TITLE: string watermark.title;static readonly WATERMARK_NOTE: string watermark.note;static readonly WATERMARK_ACCENT_COLOR: string watermark.accentColor;static readonly WATERMARK_BACKGROUND_COLOR: string watermark.backgroundColor;}StorageLink(AppStorageKeys.WORKCLOCK_VERSION)Watch(handleWorkClockVersionChanged)private workclockVersion: number 0;aboutToAppear(): void {this.refreshAllData(true);}onPageShow(): void {this.refreshAllData(false);}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.techFeatures this.service.getTechFeatures();this.usageScenes this.service.getUsageScenes();this.settings this.service.getSettings();this.syncCalendarDisplayDate(resetCalendarDate);}private bumpVersion(): void {const current: number | undefined AppStorage.get(AppStorageKeys.WORKCLOCK_VERSION) as number | undefined;AppStorage.setOrCreatenumber(AppStorageKeys.WORKCLOCK_VERSION, (current ?? 0) 1);}Key写入方读取方MAIN_TABWorkClockService.bootstrap / Index.switchTabIndex.refreshAllDataWORKCLOCK_VERSIONWorkClockService.bumpVersionIndex.StorageLink WatchWATERMARK_TEMPLATE_IDpublishSelectedWatermarkSnapshot水印页和拍照页WATERMARK_TITLE / NOTE / COLORpublishSelectedWatermarkSnapshot水印编辑页预览和保存一句话记住它AppStorage 不是数据库它只负责让页面之间共享轻量状态真正的业务数据仍然由服务层和本地存储负责。再看一层启动链路里最该盯住的三处第四篇把入口链路串起来之后真正值得在文章里再多说一遍的是启动阶段最该盯住的三处EntryAbility 的初始化、main_pages.json 的页面清单、module.json5 的入口与权限声明。只要这三处同时对齐首页才有机会在启动后稳稳落地。阶段关键动作读者看到的结果onCreate先 bootstrap 服务层本地数据和默认状态提前准备好onWindowStageCreate再 loadContent 主页面首页被稳定装载到窗口中main_pages.json声明可加载页面首页和功能页路由来源清晰module.json5声明入口和权限相机、麦克风和定位链路更完整onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);WorkClockService.getInstance().bootstrap(this.context);}onWindowStageCreate(windowStage: window.WindowStage): void {windowStage.loadContent(Routes.MAIN_PAGE, (err) {if (err.code) {hilog.error(DOMAIN, testTag, Failed to load the content. Cause: %{public}s, JSON.stringify(err));return;}});}很多真机问题看上去像页面问题实际上是入口阶段没有把服务和窗口准备好。把启动链路写清楚文章本身会更完整读者在跑项目时也更容易知道“先查哪里、后查哪里”。五、把启动链路和首页数据闭环合起来这一段把第 4 章再往前推一步EntryAbility 不只是把首页送出来还要先把服务层和 AppStorage 的轻量状态准备好。这样首页打开时拿到的不是临时壳而是一份已经可以直接渲染的快照。从这个角度看main_pages.json 和 module.json5 也不只是静态配置而是启动链路能否稳稳落地的关键边界。只要入口、页面清单和权限声明对齐首页就能沿着同一条数据链路继续工作。图4-4 启动后的数据闭环EntryAbility 先准备服务层再把首页快照交给 Index。启动阶段代码目的EntryAbility.onCreate()WorkClockService.bootstrap(context)先把持久化和默认状态准备好windowStage.loadContent()Routes.MAIN_PAGE把首页送到用户面前Index.aboutToAppear()/onPageShow()refreshAllData()首页一打开就读到最新快照WORKCLOCK_VERSIONAppStorage记录变化后触发页面重绘onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);WorkClockService.getInstance().bootstrap(this.context);}private refreshAllData(resetCalendarDate: boolean): void {this.overview this.service.getOverviewSnapshot();this.records this.service.getRecords();this.recentRecords this.service.getRecentRecords();}private handleWorkClockVersionChanged(): void {this.refreshAllData(false);}把这一层再压实一次第四章就不只是“入口能打开”而是“入口打开后马上就能看到可以继续往下跑的真实数据”。这也是留痕后面几章能够顺着写下去的前提。六、本章小结第四章把启动链路真正串了起来EntryAbility 先做服务初始化再加载首页main_pages.json 和 module.json5 各自负责页面清单和入口配置Index 通过 AppStorage 的版本号和主 Tab 状态把服务层快照渲染成用户能看到的数据。这样一来首页一打开就有内容后面的拍照、录音和记录页也都能沿着同一套状态链路工作。今日实操步骤检查点完成后看到什么查看 EntryAbility确认 onCreate 与 onWindowStageCreate 的职责知道应用为什么先进入入口再进入首页查看 bootstrap确认服务层在首页前完成初始化本地数据和模板快照已经准备好查看 Index确认 StorageLink 和 refreshAllData 的作用首页切换和刷新都能跟着版本号走如果你在真机上点开“留痕”首页能够立刻看到卡片、记录和状态说明这一章的链路已经跑通了。
第04篇|Stage模型启动链路:EntryAbility到首页加载解析
这一章只看一件事应用为什么能先把服务层准备好再把首页稳稳地加载出来。图 4-1 章首页封面Stage 模型入口与首页加载链路维度内容本章主题EntryAbility 如何在 Stage 模型下把“留痕”带到首页核心源码EntryAbility.ets、main_pages.json、module.json5、WorkClockService.ets、Index.ets阅读目标看懂启动链路、配置职责和首页数据为什么能在启动后直接出现先给结论在 Stage 模型里真正的入口不是首页页面本身而是 EntryAbility。EntryAbility 负责先 bootstrap再 loadContent首页只是最后被加载出来的那一页。本章导读这一章只看入口链路EntryAbility 怎么把首页送到用户面前main_pages.json 和 module.json5 又各自负责什么。看启动顺序怎么走。看首页为什么能一打开就有数据。看哪些配置决定首屏和路由。入口链路理顺之后再看首页的数据渲染就会自然很多。一、Stage 模型下真正的入口是 EntryAbility在这个项目里应用从桌面图标被点开之后最先被系统调用的是 EntryAbility。它不是一个普通页面而是负责把应用从“启动态”切换到“可见态”的第一道闸门。这个阶段如果没有把服务层和窗口链路准备好首页就算能渲染出来也只会是一张空壳。import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from kit.AbilityKit;import { hilog } from kit.PerformanceAnalysisKit;import { window } from kit.ArkUI;import { WorkClockService } from dynamiclibrary;import { Routes } from ../common/Routes;export default class EntryAbility extends UIAbility {onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {try {this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);WorkClockService.getInstance().bootstrap(this.context);} catch (err) {hilog.error(DOMAIN, testTag, Failed to set colorMode. Cause: %{public}s, JSON.stringify(err));}}onWindowStageCreate(windowStage: window.WindowStage): void {windowStage.loadContent(Routes.MAIN_PAGE, (err) {if (err.code) {hilog.error(DOMAIN, testTag, Failed to load the content. Cause: %{public}s, JSON.stringify(err));return;}hilog.info(DOMAIN, testTag, Succeeded in loading the content.);});}}阶段关键动作效果onCreate先切换颜色模式再调用 WorkClockService.bootstrap()服务层在首页出现之前完成准备onWindowStageCreate通过 Routes.MAIN_PAGE 加载首页入口和页面路由保持统一loadContent 失败记录 hilog 日志便于定位白屏或路由错误图 4-2 启动链路系统启动 - EntryAbility - bootstrap - 首页二、先 bootstrap再 loadContentEntryAbility.onCreate 里最关键的动作不是页面渲染而是 WorkClockService.bootstrap。它把本地持久化数据、默认模板和 AppStorage 状态先准备好再把首页交给 windowStage.loadContent。这样做的好处很直接首页第一次进入时拿到的就是可用数据而不是临时拼出来的默认壳。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();}步骤做什么结果初始化 AppStorage写入 WORKCLOCK_VERSION 和 MAIN_TAB首页和服务层有统一的轻量状态入口读取持久化状态从 WorkClockRepository 取回 JSON已有记录和模板可以直接恢复回填默认值当本地没有数据时装入默认模板和分类首次安装也不会空白发布水印快照把当前模板写回 AppStorage水印页和拍照页能同步读取这一段的关键bootstrap 不是“初始化一下就完了”而是把首页需要的默认状态、模板快照和持久化入口一次性搭好。首页之所以能直接展示内容是因为这一步已经先把底子铺好了。三、main_pages.json 和 module.json5 各管一半页面清单和 Ability 配置是两层职责。main_pages.json 只告诉系统哪些页面可以被加载module.json5 则负责声明入口 Ability、权限和启动能力。两份文件各管一半缺一块都不行。{src: [pages/Index,pages/FeatureCapturePage,pages/FeatureVoicePage,pages/FeatureWatermarkPage,pages/FeatureProjectPage,pages/FeatureRecordPage,pages/FeatureCalendarPage,pages/FeatureStatsPage,pages/FeatureSettingsPage]}{module: {name: entry,type: entry,mainElement: EntryAbility,pages: $profile:main_pages,requestPermissions: [{ name: ohos.permission.CAMERA, reason: $string:camera_permission_reason },{ name: ohos.permission.MICROPHONE, reason: $string:microphone_permission_reason },{ name: ohos.permission.APPROXIMATELY_LOCATION, reason: $string:location_permission_reason },{ name: ohos.permission.LOCATION, reason: $string:location_permission_reason }]}}图 4-3 配置关系main_pages.json、module.json5 与 EntryAbility 的分工配置项作用本章关注点mainElement指向入口 Ability系统先找到 EntryAbility再决定加载什么页面pages绑定页面清单首页和功能页都从这里被注册requestPermissions声明运行权限相机、麦克风和定位都在这里提前声明skills / actions支持桌面入口保证桌面点击能够进入应用四、首页为什么一打开就有数据首页不是自己去拉一份孤立的数据而是通过 AppStorage 里的轻量状态和服务层的快照来渲染。WorkClockService 负责维护真实数据Index 负责在页面生命周期里把这些数据读出来再渲染成首页、记录和统计卡片。export class AppStorageKeys {static readonly MAIN_TAB: string main.tab;static readonly WORKCLOCK_VERSION: string workclock.version;static readonly WATERMARK_TEMPLATE_ID: string watermark.templateId;static readonly WATERMARK_CATEGORY: string watermark.category;static readonly WATERMARK_TITLE: string watermark.title;static readonly WATERMARK_NOTE: string watermark.note;static readonly WATERMARK_ACCENT_COLOR: string watermark.accentColor;static readonly WATERMARK_BACKGROUND_COLOR: string watermark.backgroundColor;}StorageLink(AppStorageKeys.WORKCLOCK_VERSION)Watch(handleWorkClockVersionChanged)private workclockVersion: number 0;aboutToAppear(): void {this.refreshAllData(true);}onPageShow(): void {this.refreshAllData(false);}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.techFeatures this.service.getTechFeatures();this.usageScenes this.service.getUsageScenes();this.settings this.service.getSettings();this.syncCalendarDisplayDate(resetCalendarDate);}private bumpVersion(): void {const current: number | undefined AppStorage.get(AppStorageKeys.WORKCLOCK_VERSION) as number | undefined;AppStorage.setOrCreatenumber(AppStorageKeys.WORKCLOCK_VERSION, (current ?? 0) 1);}Key写入方读取方MAIN_TABWorkClockService.bootstrap / Index.switchTabIndex.refreshAllDataWORKCLOCK_VERSIONWorkClockService.bumpVersionIndex.StorageLink WatchWATERMARK_TEMPLATE_IDpublishSelectedWatermarkSnapshot水印页和拍照页WATERMARK_TITLE / NOTE / COLORpublishSelectedWatermarkSnapshot水印编辑页预览和保存一句话记住它AppStorage 不是数据库它只负责让页面之间共享轻量状态真正的业务数据仍然由服务层和本地存储负责。再看一层启动链路里最该盯住的三处第四篇把入口链路串起来之后真正值得在文章里再多说一遍的是启动阶段最该盯住的三处EntryAbility 的初始化、main_pages.json 的页面清单、module.json5 的入口与权限声明。只要这三处同时对齐首页才有机会在启动后稳稳落地。阶段关键动作读者看到的结果onCreate先 bootstrap 服务层本地数据和默认状态提前准备好onWindowStageCreate再 loadContent 主页面首页被稳定装载到窗口中main_pages.json声明可加载页面首页和功能页路由来源清晰module.json5声明入口和权限相机、麦克风和定位链路更完整onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);WorkClockService.getInstance().bootstrap(this.context);}onWindowStageCreate(windowStage: window.WindowStage): void {windowStage.loadContent(Routes.MAIN_PAGE, (err) {if (err.code) {hilog.error(DOMAIN, testTag, Failed to load the content. Cause: %{public}s, JSON.stringify(err));return;}});}很多真机问题看上去像页面问题实际上是入口阶段没有把服务和窗口准备好。把启动链路写清楚文章本身会更完整读者在跑项目时也更容易知道“先查哪里、后查哪里”。五、把启动链路和首页数据闭环合起来这一段把第 4 章再往前推一步EntryAbility 不只是把首页送出来还要先把服务层和 AppStorage 的轻量状态准备好。这样首页打开时拿到的不是临时壳而是一份已经可以直接渲染的快照。从这个角度看main_pages.json 和 module.json5 也不只是静态配置而是启动链路能否稳稳落地的关键边界。只要入口、页面清单和权限声明对齐首页就能沿着同一条数据链路继续工作。图4-4 启动后的数据闭环EntryAbility 先准备服务层再把首页快照交给 Index。启动阶段代码目的EntryAbility.onCreate()WorkClockService.bootstrap(context)先把持久化和默认状态准备好windowStage.loadContent()Routes.MAIN_PAGE把首页送到用户面前Index.aboutToAppear()/onPageShow()refreshAllData()首页一打开就读到最新快照WORKCLOCK_VERSIONAppStorage记录变化后触发页面重绘onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);WorkClockService.getInstance().bootstrap(this.context);}private refreshAllData(resetCalendarDate: boolean): void {this.overview this.service.getOverviewSnapshot();this.records this.service.getRecords();this.recentRecords this.service.getRecentRecords();}private handleWorkClockVersionChanged(): void {this.refreshAllData(false);}把这一层再压实一次第四章就不只是“入口能打开”而是“入口打开后马上就能看到可以继续往下跑的真实数据”。这也是留痕后面几章能够顺着写下去的前提。六、本章小结第四章把启动链路真正串了起来EntryAbility 先做服务初始化再加载首页main_pages.json 和 module.json5 各自负责页面清单和入口配置Index 通过 AppStorage 的版本号和主 Tab 状态把服务层快照渲染成用户能看到的数据。这样一来首页一打开就有内容后面的拍照、录音和记录页也都能沿着同一套状态链路工作。今日实操步骤检查点完成后看到什么查看 EntryAbility确认 onCreate 与 onWindowStageCreate 的职责知道应用为什么先进入入口再进入首页查看 bootstrap确认服务层在首页前完成初始化本地数据和模板快照已经准备好查看 Index确认 StorageLink 和 refreshAllData 的作用首页切换和刷新都能跟着版本号走如果你在真机上点开“留痕”首页能够立刻看到卡片、记录和状态说明这一章的链路已经跑通了。