HarmonyOS 6.0 V2 状态管理实战(下)- 基于 AppStore + TodayStore 拆当前项目的 Store 分层

系列文章 :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 作为组合对象存在

authStoresyncStoretodayStore 都被组合进来,但不直接复制成扁平字段。

按当前代码结构,这样做至少有两个直接结果:

  • 模块边界更清晰,登录就是登录,同步就是同步,今日记录就是今日记录

  • 后续扩展新能力时,只要新增 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';
  }
}

对照当前实现,这个方法里可以直接看出三件事:

  1. 输入校验在业务 Store 内完成,而不是交给页面兜底

  2. 创建和更新都走统一的数据模型方法

  3. 成功和失败都明确落到状态机分支

所以当前页面并不需要理解"保存细节",只要根据 statuserrorMessage 渲染即可。


为什么 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 的职责是"今天记录"的本地业务状态

  • 同步是跨模块能力,依赖 authStoresyncStorerepo

所以从当前依赖关系看,把这个动作留在 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 细节

调用方不用反复写 getPreferencesflush、异常处理。

2. 限制上层乱用系统能力

只开放字符串读写和删除,意味着上层不会把这里当数据库乱玩。

3. 后续替换实现成本低

以后你想把本地存储换成更复杂的方案,只要 Repo 层契约不变,业务层影响就很小。

对这个仓库来说,这层的意义主要还是隔离系统存储细节。


当前实现里能直接看到的状态边界与渲染影响

这一节不讨论跑分和专项优化,只看当前代码里已经存在的状态边界会怎样影响页面刷新范围和职责划分。

按当前实现,比较直接能看到的是下面这些点。

1. @Trace 只标记必要字段

TodayStore 里没有把 repolastStableStatus 标成响应式,这就已经是在减少无意义追踪。

2. 页面读取的主要是 today 这条业务状态

TodayPage 接收的是 TodayStore,但内部读取的主要是:

  • status

  • dateKey

  • text

  • errorMessage

  • draftText

它没有把登录、同步等其他业务状态直接揉进 TodayPage

3. 页面没有直接写异步持久化或同步

保存和清空最终都会回到 AppStore / TodayStore,而不是在 build() 里直接碰系统 API。

4. 输入缓存和正式数据分离

draftTexttext 分开,避免用户每输入一个字都污染已保存内容,这不仅是交互更稳,也是避免不必要状态回退复杂度的关键。


当前项目为什么把页面切换也放进 AppStore

AppStore 里有两个非常简单的方法:

复制代码
openSettings(): void {
  this.currentPage = 'settings';
}

openToday(): void {
  this.currentPage = 'today';
}

你可能会觉得这也值得写?

因为这表示"当前页是什么"被集中放在 AppStore.currentPage 上,而不是散落在页面局部变量里。

当前项目现在只有 todaysettings 两页,从现有代码看,这种写法至少建立了下面这条规则:

  • 页面切换通过 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()


当前项目这套分层,具体解决了什么

如果只看这个仓库现在已经有的代码,这套分层至少解决了三件很具体的事:

  1. Index 不需要知道本地存储和同步细节,只负责切换页面和传动作。

  2. TodayPage 不需要知道登录态和同步状态,只围绕 TodayStore 渲染。

  3. TodayRepo / Prefs 把 key 规则、JSON 序列化和 Preferences 细节收了起来,TodayStore 不用直接处理原始字符串。


小结

这一篇最核心的结论可以直接落到当前仓库上:

这个项目里真正需要看清的,不只是装饰器怎么写,而是谁负责页面、谁负责 today 业务、谁负责应用级协作。

「今天空白」项目里这套分工很值得照着学:

  • AppStore 负责应用级编排

  • TodayStore 负责今日记录业务

  • TodayRepo 负责数据格式与键规则

  • Prefs 负责系统存储最小封装

  • 页面只负责读状态和触发动作

下一篇本小姐会继续把 TodayPageEditSheet 拆开,只看这个仓库当前页面是怎么搭布局、弹层和样式层次的。( ̄へ ̄)

相关推荐
黑鲨吃西瓜2 小时前
鸿蒙开发中V2状态管理的使用(下)
harmonyos·鸿蒙·deveco studio
江湖有缘3 小时前
基于开发者空间部署Eigenfocus项目管理工具【华为开发者空间】
运维·服务器·华为
钛态13 小时前
Flutter for OpenHarmony:mockito 单元测试的替身演员,轻松模拟复杂依赖(测试驱动开发必备) 深度解析与鸿蒙适配指南
服务器·驱动开发·安全·flutter·华为·单元测试·harmonyos
前端不太难15 小时前
从系统调度看鸿蒙的性能优势来源
华为·状态模式·harmonyos
小白学鸿蒙16 小时前
串口通信发送后无响应|极简排查步骤(实战总结)
华为·harmonyos
国医中兴19 小时前
ClickHouse的数据模型设计:从理论到实践
flutter·harmonyos·鸿蒙·openharmony
ICT系统集成阿祥20 小时前
小型企业WIFI配置方案,附华为企业 WiFi 完整配置案例!
华为
晚霞的不甘21 小时前
HarmonyOS ArkTS 进阶实战:深入理解边距、边框与嵌套布局
前端·计算机视觉·华为·智能手机·harmonyos
国医中兴1 天前
ClickHouse数据导入导出最佳实践:从性能到可靠性
flutter·harmonyos·鸿蒙·openharmony