系列文章:HarmonyOS 6.0 实战开发 - 「今天空白」应用 第8篇 / 共30篇 发布时间:2026-04-06 阅读时长:18分钟 难度:(进阶)


本文导读

做移动应用时,很多人一聊到数据持久化,第一反应就是:

  • 要不要上数据库

  • 要不要做历史表

  • 要不要顺便把同步结构也设计了

但如果你真的回到「今天空白」当前项目,会发现问题根本没那么复杂。

这个应用最核心的数据只有一件事:

今天有没有记录,记录了什么。

所以这一篇不讲一堆脱离仓库现实的存储教程,而是只按当前项目真实需求来拆:

  • 当前产品边界到底要求保存什么

  • 为什么这类数据适合先落在 Preferences

  • 现在这条存储链路在仓库里是怎么组织的

  • 为什么当前实现刻意没有引入更重的存储方案

本文对应的真实代码和文档主要来自这些位置:

  • 开发约定.md

  • docs/数据与存储.md

  • frontend/entry/src/main/ets/common/storage/Prefs.ets

  • frontend/entry/src/main/ets/features/today/TodayRepo.ets

  • frontend/entry/src/main/ets/features/today/TodayStore.ets

  • frontend/entry/src/main/ets/features/app/AppStore.ets


先别急着选技术,先看当前项目到底要存什么

如果只看 README.md开发约定.md,这个项目的产品边界非常清楚:

  • 每天只记录一件事

  • 当天最多 1 条记录

  • 记录内容是非空文本

  • 默认离线,本地持久化优先

  • 不做目标管理、OKR、成长系统

这几个条件放在一起,直接决定了当前数据形态是极度收敛的。

当前真正要解决的问题,不是“如何管理很多复杂实体”,而是:

  1. 今天这一天有没有对应记录

  2. 有的话,把它读出来

  3. 改了以后覆盖写回去

  4. 删除以后恢复到“今天空白”

如果数据问题本身就这么简单,一上来就堆数据库表结构、历史索引、复杂查询,基本都属于过度设计。


当前仓库里的数据模型非常小

docs/数据与存储.md 已经把 DailyEntry 模型写得很清楚:

  • date: YYYY-MM-DD

  • text: string

  • createdAt: number

  • updatedAt: number

同时还有两个关键约束:

  • 同一天只能有 1 条 DailyEntry

  • text.trim().length === 0 视为“空白”

这意味着当前仓库既没有:

  • 一对多关系

  • 复杂筛选

  • 模糊搜索

  • 分页列表

  • 统计报表

从数据结构角度说,它更像是“按日期 key 读写一条值”,而不是“围绕多张表做查询系统”。

这就是为什么存储选型不能脱离业务形态来看。


当前项目的本地存储链路其实很短

如果按真实代码往下追,当前链路大致是:

AppStore
  -> TodayStore
    -> TodayRepo
      -> Prefs
        -> HarmonyOS Preferences

AppStore 构造函数里已经把这条链路串起来了:

constructor(context: common.UIAbilityContext) {
  const prefs = new Prefs(context);
  const api = new ApiClient(API_BASE_URL);
  this.repo = new TodayRepo(prefs);
  this.todayStore = new TodayStore(this.repo);
  this.authStore = new AuthStore(prefs, api);
  this.syncStore = new SyncStore(prefs, api);
}

最重要的一点是:

今天记录的本地持久化,当前依然是通过 Prefs -> TodayRepo 这一层完成的。

哪怕仓库已经有登录和同步能力,本地存储依然没有被替换掉,而是继续承担最基础的“今天这条记录落哪里”这个职责。

也就是说,当前项目的设计不是“有同步了,所以本地可以不要”,而是:

  • 先把本地记录存稳

  • 登录和同步是在这条本地链路之上追加能力


为什么按当前实现看,Preferences 是顺理成章的选择

如果只根据当前项目需求、项目文档和现有实现往回看,Preferences 的适配度几乎是直接对口的。

1. 当前数据访问模式本来就是按 key 读写单条

仓库里没有“查询最近 30 天记录列表”的本地实现,也没有多条件筛选。

最核心的操作只有:

  • 读取今天这一条

  • 写回今天这一条

  • 删除今天这一条

这和 Preferences 这种“按 key 存取简单值”的模型正好契合。

2. 当前项目不需要关系型查询能力

如果你的页面需要:

  • 多字段检索

  • 排序

  • 条件过滤

  • 联表关系

那数据库当然会变得更有价值。

但「今天空白」现在没有这些需求。当前仓库甚至连“历史记录页”都没有,更别说统计分析了。所以数据库能力在这个阶段根本派不上用场。

3. 当前项目更看重简单、稳定、可维护

开发约定.md 明确强调:

  • KISS

  • YAGNI

  • 为 MVP 服务

在这种前提下,选型的关键不是“哪种技术更显得高级”,而是“哪种方案最少引入额外复杂度”。

Preferences 的好处就在这里:

  • 够简单

  • 够直接

  • 足够支撑当前一条记录的存取


为什么当前实现没有上更重的本地方案

这一篇既然叫“存储方案实战”,也要把“不选什么”说清楚。但这里仍然只按当前仓库现实来分析,不讲脱离项目的空泛对比。

1. 当前没必要上关系型数据库

数据库最擅长的是结构化查询和更复杂的数据关系。

但当前项目的本地数据只有:

  • 一个按日期定位的对象

  • 一个文本字段

  • 两个时间戳

而且访问方式是“一天一条,覆盖更新”。

这种情况下,如果引入数据库,新增的主要不是价值,而是:

  • 表结构设计

  • 建表与迁移

  • 查询封装

  • 更多出错点

2. 当前没必要自己管理文件存储

如果改成文件方案,你至少还要自己处理:

  • 文件命名规则

  • 读写时机

  • 序列化和反序列化

  • 文件不存在时的回退逻辑

而当前项目本质上只是存一个 JSON 字符串。既然 Preferences 已经能稳妥解决,就没必要换成更手动的文件方案。

3. 当前也没必要把“分布式 / 同步”当成基础存储

仓库现在确实已经有 SyncStore,但从代码结构看,它是附加能力,不是替代本地存储的底层。

例如 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);
    }
  }
}

这段逻辑已经说明:

  • 本地先保存

  • 保存成功后,再考虑远端同步

所以在当前项目里,同步不是基础存储方案,而是本地方案之上的扩展动作。


当前仓库把 key 规则和 value 规则都定死了

docs/数据与存储.mdTodayRepo.ets 对 key/value 规则是一致的。

先看 key:

const KEY_PREFIX = 'daily_entry_';
​
function toKey(dateKey: string): string {
  return `${KEY_PREFIX}${dateKey}`;
}

也就是说,每一天的记录都落在固定 key 上:

daily_entry_YYYY-MM-DD

再看 value,当前项目用的是 JSON 字符串:

async set(entry: DailyEntry): Promise<void> {
  await this.prefs.setString(toKey(entry.date), JSON.stringify(entry));
}

这个选择也非常符合项目规模:

  • 结构简单

  • 序列化直接

  • 不需要额外 schema 层

对“每天只记录一件事”这种场景来说,这已经足够清晰。


在这个项目里,空白判定比“用什么存储”更重要

当前存储设计最值得注意的地方,其实不是“选了 Preferences”,而是空白语义被定义得非常明确

docs/数据与存储.md 里写得很死:

满足任一条件即 blank

  • 不存在对应 key

  • text 不存在

  • text.trim().length === 0

这套规则在 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;
  }
}

注意这里的设计取向:

  • 缺 key,返回 null

  • JSON 解析失败,返回 null

  • 字段不完整,返回 null

  • 文本为空白,返回 null

然后上层 TodayStore.refresh()null 统一解释成:

  • text = ''

  • updatedAt = 0

  • status = 'blank'

这就让“今天空白”不只是一个 UI 文案,而是整条数据链路里清晰一致的业务语义。


当前选型其实同时满足了 KISS 和 YAGNI

把当前仓库的存储决策翻译成工程语言,大致就是:

KISS

只做“今天一条记录”的最短路径:

  • 用固定 key 找今天

  • 用 JSON 保存对象

  • 读不到或无效就当空白

YAGNI

当前还没有这些明确需求:

  • 本地历史列表页

  • 复杂搜索

  • 统计报表

  • 多实体关系

  • 本地多表事务

既然没有,就不要提前为这些可能性把存储层写复杂。

这不是“偷懒”,而是项目边界感足够清楚。


当前仓库故意没做的几件事

如果你只看现有代码,还能发现它明确没做下面这些本地存储扩展:

1. 没有本地迁移体系

现在的 DailyEntry 结构很小,仓库里没有版本迁移逻辑。

2. 没有多日期范围的批量读取接口

TodayRepo 只暴露:

  • get(dateKey)

  • set(entry)

  • remove(dateKey)

没有“获取所有记录”。

3. 没有把同步层混进本地 Repo

同步逻辑在 AppStoreSyncStore,本地 Repo 仍然只关心本地 key/value。

这些“没做”的地方,恰恰说明当前实现边界收得住。


小结

这一篇如果只按「今天空白」当前仓库来总结,结论其实很直接:

Preferences 不是因为它最万能才被选中,而是因为这个项目当前的数据问题本来就很小,它正好够用。

当前选型背后的核心理由可以归纳成 4 点:

  • 每天只有 1 条记录,本地访问模式就是按 key 读写单条

  • 当前没有本地复杂查询需求,不值得引数据库

  • 本地存储仍然是基础链路,同步只是附加能力

  • key 规则、value 规则和空白语义都已经在仓库里固定下来

下一篇我们继续顺着这条链路往下拆,不再停留在“为什么选 Preferences”,而是直接看当前项目怎么用 Prefs + TodayRepo + DailyEntry 把 Repository 模式真正落地出来。(^_^)b

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐