HarmonyOS 数据持久化方案实战 - 基于「今天空白」当前需求拆 Preferences 选型与存储边界
做移动应用时,很多人一聊到数据持久化,第一反应就是:要不要上数据库要不要做历史表要不要顺便把同步结构也设计了但如果你真的回到「今天空白」当前项目,会发现问题根本没那么复杂。这个应用最核心的数据只有一件事:今天有没有记录,记录了什么。所以这一篇不讲一堆脱离仓库现实的存储教程,而是只按当前项目真实需求来拆:当前产品边界到底要求保存什么为什么这类数据适合先落在 Preferences现在这条存储链路在仓
系列文章: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、成长系统
这几个条件放在一起,直接决定了当前数据形态是极度收敛的。
当前真正要解决的问题,不是“如何管理很多复杂实体”,而是:
-
今天这一天有没有对应记录
-
有的话,把它读出来
-
改了以后覆盖写回去
-
删除以后恢复到“今天空白”
如果数据问题本身就这么简单,一上来就堆数据库表结构、历史索引、复杂查询,基本都属于过度设计。
当前仓库里的数据模型非常小
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/数据与存储.md 和 TodayRepo.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
同步逻辑在 AppStore 和 SyncStore,本地 Repo 仍然只关心本地 key/value。
这些“没做”的地方,恰恰说明当前实现边界收得住。
小结
这一篇如果只按「今天空白」当前仓库来总结,结论其实很直接:
Preferences 不是因为它最万能才被选中,而是因为这个项目当前的数据问题本来就很小,它正好够用。
当前选型背后的核心理由可以归纳成 4 点:
-
每天只有 1 条记录,本地访问模式就是按 key 读写单条
-
当前没有本地复杂查询需求,不值得引数据库
-
本地存储仍然是基础链路,同步只是附加能力
-
key 规则、value 规则和空白语义都已经在仓库里固定下来
下一篇我们继续顺着这条链路往下拆,不再停留在“为什么选 Preferences”,而是直接看当前项目怎么用 Prefs + TodayRepo + DailyEntry 把 Repository 模式真正落地出来。(^_^)b
更多推荐

所有评论(0)