系列文章 :HarmonyOS 6.0 实战开发 - 「今天空白」应用 第5篇 / 共30篇 发布时间 :2026-03-26 阅读时长 :20分钟 难度:(进阶)
本文导读
上一篇我们把 V2 装饰器的基础搭好了,但如果项目一旦变大,只会写 @ObservedV2 还远远不够。
放到当前仓库里,真正需要看清的是这几个问题:
-
页面状态放哪里
-
存储逻辑放哪里
-
登录、同步、路由切换这类"跨模块动作"由谁协调
-
V2 响应式项目怎样避免过度渲染和职责混乱
这篇文章继续基于「今天空白」真实代码,拆出项目里的 Store 分层:
-
AppStore: 应用级编排层 -
TodayStore: 今日记录业务状态层 -
TodayRepo: 数据访问层 -
Prefs: HarmonyOS Preferences 最小封装层
主角文件如下:
-
frontend/entry/src/main/ets/features/app/AppStore.ets -
frontend/entry/src/main/ets/features/today/TodayStore.ets -
frontend/entry/src/main/ets/features/today/TodayRepo.ets -
frontend/entry/src/main/ets/common/storage/Prefs.ets -
frontend/entry/src/main/ets/pages/Index.ets
先说结论: 当前项目的 Store 分层不只是"搞个 class"
很多教程说"Store 模式",最后只是写了一个类,然后把页面变量往里一塞。这种写法撑不过两个需求迭代。
在这个项目里,Store 分层主要解决的是这几个实际问题:
-
页面只表达 UI 和用户意图
-
业务 Store 只维护领域状态与动作
-
AppStore 负责跨模块协调
-
Repo 负责数据格式转换
-
Prefs 负责和系统存储打交道
这几层一旦拆清楚,后面接入同步、加密、认证时,改动就不会失控。
应用级入口: AppStore 才是编排中枢
先看 AppStore 的核心结构:
@ObservedV2
export class AppStore {
@Trace currentPage: 'today' | 'settings' = 'today';
authStore: AuthStore;
syncStore: SyncStore;
todayStore: TodayStore;
private repo: TodayRepo;
private initialized = false;
}
这里最有意思的点有两个:
1. AppStore 当前只追踪 currentPage
它本身用了 @ObservedV2,但只有 currentPage 加了 @Trace。
为什么?
因为 Index.ets 里直接依赖 AppStore 字段做分支渲染的,当前就是页面切换这一处:
if (this.appStore.currentPage === 'settings') {
Settings({ appStore: this.appStore });
} else {
TodayPage({
store: this.appStore.todayStore,
onSave: (text: string) => this.appStore.saveToday(text),
onClear: () => this.appStore.clearToday(),
onOpenSettings: () => this.appStore.openSettings()
});
}
至少从当前代码看,AppStore 没有被写成一个把所有字段都摊平暴露的全局桶。
2. 其他子 Store 作为组合对象存在
authStore、syncStore、todayStore 都被组合进来,但不直接复制成扁平字段。
按当前代码结构,这样做至少有两个直接结果:
-
模块边界更清晰,登录就是登录,同步就是同步,今日记录就是今日记录
-
后续扩展新能力时,只要新增 Store,不需要把
AppStore改成几百行的大泥球
也就是说,当前项目更偏向"组合多个 Store",而不是把所有状态揉成一个大对象。
初始化链路: 为什么 init() 不该写在页面里
项目首页 Index.ets 是这么启动的:
@Local appStore: AppStore = new AppStore(this.getUIContext().getHostContext() as common.UIAbilityContext);
aboutToAppear(): void {
this.appStore.init();
}
init() 的内容是:
async init(): Promise<void> {
if (this.initialized) {
return;
}
this.initialized = true;
await this.authStore.restore();
await this.syncStore.restore();
await this.todayStore.refresh();
if (this.authStore.isLoggedIn) {
await this.syncStore.ensureParams(this.authStore.accessToken);
}
}
这一段最能说明当前仓库为什么要保留 AppStore 这一层。
如果把这些初始化逻辑直接塞进 Index.ets,页面就得知道:
-
账号状态怎么恢复
-
同步状态怎么恢复
-
今天记录怎么加载
-
已登录时还要补哪些同步参数
那页面就不再是页面,而是启动脚本了。
现在的写法是 Index 只负责调一次 init(),启动顺序则收在 AppStore 里。
TodayStore: 业务 Store 只做"今天"这件事
TodayStore 的职责非常纯:
-
加载今天记录
-
打开编辑器
-
取消编辑
-
保存今天记录
-
清空今天记录
没有路由,没有登录,没有网络,没有同步调度。
看 save() 的核心逻辑:
async save(nextText: string): Promise<void> {
const normalized = normalizeText(nextText);
if (isBlankText(normalized)) {
this.errorMessage = '内容不能为空';
return;
}
this.status = 'saving';
this.errorMessage = '';
const nowMs = Date.now();
try {
const existing = await this.repo.get(this.dateKey);
const entry = existing
? updateEntry(existing, normalized, nowMs)
: createEntry(this.dateKey, normalized, nowMs);
await this.repo.set(entry);
this.text = entry.text;
this.updatedAt = entry.updatedAt;
this.draftText = '';
this.status = 'recorded';
this.lastStableStatus = 'recorded';
} catch (_err) {
this.errorMessage = '保存失败';
this.status = 'editing';
}
}
对照当前实现,这个方法里可以直接看出三件事:
-
输入校验在业务 Store 内完成,而不是交给页面兜底
-
创建和更新都走统一的数据模型方法
-
成功和失败都明确落到状态机分支
所以当前页面并不需要理解"保存细节",只要根据 status 和 errorMessage 渲染即可。
为什么 saveToday() 不直接写成 todayStore.save()
看到这里,你可能会问:
既然 TodayStore 已经有 save() 了,为什么 Index.ets 里不直接传 onSave: text => store.save(text),反而要绕到 AppStore.saveToday()?
因为在当前项目里,"保存今天"确实不是单一动作,而是一个动作链:
async saveToday(text: string): Promise<void> {
await this.todayStore.save(text);
if (this.todayStore.status !== 'recorded') {
return;
}
if (this.authStore.isLoggedIn && this.syncStore.isReady) {
const entry = await this.repo.get(this.todayStore.dateKey);
if (entry) {
await this.syncStore.pushEntry(entry, this.authStore.accessToken);
}
}
}
这里同时包含:
-
本地保存
-
保存成功判断
-
登录态判断
-
同步能力判断
-
读取最新记录
-
推送远端
这些逻辑显然不该放进 TodayPage,也不该污染 TodayStore。
原因很明确:
-
TodayStore的职责是"今天记录"的本地业务状态 -
同步是跨模块能力,依赖
authStore、syncStore和repo
所以从当前依赖关系看,把这个动作留在 AppStore 更贴近仓库现在的分层。
这也是 AppStore 在当前项目里的一个明确作用: 收拢跨边界协作。
数据访问层: TodayRepo 为什么不能省
有些项目为了"图快",会让 Store 直接操作 Preferences:
const pref = await preferences.getPreferences(...)
await pref.put(...)
本项目没有这么写,而是多拆了一层 TodayRepo:
export class TodayRepo {
async get(dateKey: string): Promise<DailyEntry | null> {
const raw = await this.prefs.getString(toKey(dateKey));
if (!raw) {
return null;
}
try {
const parsed = JSON.parse(raw) as DailyEntry;
return isValidEntry(parsed) ? parsed : null;
} catch (_err) {
return null;
}
}
async set(entry: DailyEntry): Promise<void> {
await this.prefs.setString(toKey(entry.date), JSON.stringify(entry));
}
}
放到当前实现里,这一层的作用主要是:
-
key 生成规则集中管理
-
JSON 序列化/反序列化逻辑集中管理
-
数据合法性校验集中管理
-
上层拿到的是
DailyEntry | null,而不是脏字符串
这让 TodayStore 可以一直停留在业务语言层,而不是陷进字符串和系统 API 细节里。
再往下拆: Prefs 为什么只暴露最小能力
Prefs.ets 很短,只有三类公开能力:
async getString(key: string): Promise<string | null>
async setString(key: string, value: string): Promise<void>
async delete(key: string): Promise<void>
很多人会觉得这层太薄了,没必要。
其实恰恰相反,这种"薄封装"非常值得保留,原因有三个:
1. 屏蔽 HarmonyOS Preferences 细节
调用方不用反复写 getPreferences、flush、异常处理。
2. 限制上层乱用系统能力
只开放字符串读写和删除,意味着上层不会把这里当数据库乱玩。
3. 后续替换实现成本低
以后你想把本地存储换成更复杂的方案,只要 Repo 层契约不变,业务层影响就很小。
对这个仓库来说,这层的意义主要还是隔离系统存储细节。
当前实现里能直接看到的状态边界与渲染影响
这一节不讨论跑分和专项优化,只看当前代码里已经存在的状态边界会怎样影响页面刷新范围和职责划分。
按当前实现,比较直接能看到的是下面这些点。
1. @Trace 只标记必要字段
TodayStore 里没有把 repo、lastStableStatus 标成响应式,这就已经是在减少无意义追踪。
2. 页面读取的主要是 today 这条业务状态
TodayPage 接收的是 TodayStore,但内部读取的主要是:
-
status -
dateKey -
text -
errorMessage -
draftText
它没有把登录、同步等其他业务状态直接揉进 TodayPage。
3. 页面没有直接写异步持久化或同步
保存和清空最终都会回到 AppStore / TodayStore,而不是在 build() 里直接碰系统 API。
4. 输入缓存和正式数据分离
draftText 和 text 分开,避免用户每输入一个字都污染已保存内容,这不仅是交互更稳,也是避免不必要状态回退复杂度的关键。
当前项目为什么把页面切换也放进 AppStore
AppStore 里有两个非常简单的方法:
openSettings(): void {
this.currentPage = 'settings';
}
openToday(): void {
this.currentPage = 'today';
}
你可能会觉得这也值得写?
因为这表示"当前页是什么"被集中放在 AppStore.currentPage 上,而不是散落在页面局部变量里。
当前项目现在只有 today 和 settings 两页,从现有代码看,这种写法至少建立了下面这条规则:
-
页面切换通过
AppStore暴露的方法完成 -
Index根据currentPage做分支渲染 -
页面内部不再额外维护一套页面切换布尔值
这是否还要继续沿用,后面可以再看项目复杂度,但当前实现确实是这么组织的。
一次前后台切换,也能看出 Store 分层是否合理
AppStore.onForeground() 代码如下:
async onForeground(): Promise<void> {
await this.todayStore.refresh();
if (this.authStore.isLoggedIn && this.syncStore.isReady) {
await this.pullToday();
}
}
放到当前项目里,这段逻辑表达的是:
-
用户切回前台时,要先刷新本地今日状态
-
如果已经登录并配置了同步,还要尝试拉取远端最新数据
如果没有 Store 分层,这种逻辑通常会散落在:
-
页面生命周期里一点
-
网络模块里一点
-
存储模块里一点
最后任何一个地方出 bug,你根本追不清入口。
而在当前结构里,前后台切换后的处理被收口到 AppStore,页面只负责调用 onForeground()。
当前项目这套分层,具体解决了什么
如果只看这个仓库现在已经有的代码,这套分层至少解决了三件很具体的事:
-
Index不需要知道本地存储和同步细节,只负责切换页面和传动作。 -
TodayPage不需要知道登录态和同步状态,只围绕TodayStore渲染。 -
TodayRepo/Prefs把 key 规则、JSON 序列化和 Preferences 细节收了起来,TodayStore不用直接处理原始字符串。
小结
这一篇最核心的结论可以直接落到当前仓库上:
这个项目里真正需要看清的,不只是装饰器怎么写,而是谁负责页面、谁负责 today 业务、谁负责应用级协作。
「今天空白」项目里这套分工很值得照着学:
-
AppStore负责应用级编排 -
TodayStore负责今日记录业务 -
TodayRepo负责数据格式与键规则 -
Prefs负责系统存储最小封装 -
页面只负责读状态和触发动作
下一篇本小姐会继续把 TodayPage 和 EditSheet 拆开,只看这个仓库当前页面是怎么搭布局、弹层和样式层次的。( ̄へ ̄)