这一章只看一件事:应用为什么能先把服务层准备好,再把首页稳稳地加载出来。
![]()
图 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,再 loadContent
EntryAbility.onCreate 里最关键的动作不是页面渲染,而是 WorkClockService.bootstrap。它把本地持久化数据、默认模板和 AppStorage 状态先准备好,再把首页交给 windowStage.loadContent。这样做的好处很直接:首页第一次进入时拿到的就是可用数据,而不是临时拼出来的默认壳。
bootstrap(context: common.UIAbilityContext): void { AppStorage.setOrCreate<number>(AppStorageKeys.WORKCLOCK_VERSION, 0); AppStorage.setOrCreate<string>(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.setOrCreate<number>(AppStorageKeys.WORKCLOCK_VERSION, (current ?? 0) + 1); } |
Key | 写入方 | 读取方 |
MAIN_TAB | WorkClockService.bootstrap / Index.switchTab | Index.refreshAllData |
WORKCLOCK_VERSION | WorkClockService.bumpVersion | Index.@StorageLink + @Watch |
WATERMARK_TEMPLATE_ID | publishSelectedWatermarkSnapshot | 水印页和拍照页 |
WATERMARK_TITLE / NOTE / COLOR | publishSelectedWatermarkSnapshot | 水印编辑页预览和保存 |
一句话记住它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_VERSION | AppStorage | 记录变化后触发页面重绘 |
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 的作用 | 首页切换和刷新都能跟着版本号走 |
如果你在真机上点开“留痕”,首页能够立刻看到卡片、记录和状态,说明这一章的链路已经跑通了。