鸿蒙新特性:Preferences 数据持久化实战 — 构建自动保存草稿箱的完整方案
引言
移动应用中最令人沮丧的体验之一是什么?花了 20 分钟写的笔记,因为一个意外返回手势或来电中断,全部消失了。用户对"数据丢失"的容忍度为零——这不是功能缺失,这是信任崩塌。
数据持久化,即将应用数据保存到设备存储中以便下次启动时恢复,是每个移动开发者迟早要面对的问题。HarmonyOS 为开发者提供了多层级的数据持久化方案:轻量级键值存储(Preferences)、关系型数据库(RelationalStore)、以及文件存储。其中,Preferences 是最轻量、最易用的方案——适合保存用户设置、草稿内容、阅读进度等小型结构化数据。
本文将通过构建一个完整的"草稿箱"应用,深入讲解 @ohos.data.preferences API 的实战使用。草稿箱支持多草稿管理、自动保存(2 秒防抖)、应用重启后恢复、以及草稿的创建、切换和删除。全程代码驱动,不堆砌理论。
读完本文你将能够:
- 使用
getPreferencesSync创建和获取 Preferences 实例 - 掌握
putSync/getSync/flushSync等同步 API 的使用 - 实现"输入即保存"的自动持久化模式
- 理解 JSON 序列化在 ArkTS 严格类型系统下的处理方式
- 构建完整的草稿 CRUD(创建、读取、切换、删除)
Preferences API 全景
什么是 Preferences?
Preferences 是一个轻量级的键值对(Key-Value)存储系统。你可以把它理解为一个"持久化的 Map"——你可以用字符串作为 key 存储数据(字符串、数字、布尔值),通过 key 读取数据,数据保存在设备的 XML 文件中,应用重启后依然存在。
Preferences 的设计目标不是替代数据库,而是覆盖那些"我不想为此建表"的轻量场景:
- 用户设置(字体大小、主题颜色、通知开关)
- 草稿内容(笔记、评论、表单的半成品状态)
- 应用状态(首次启动标记、上次使用时间)
- 阅读进度(当前章节、滚动位置)
核心 API 速览
| API | 类型 | 用途 |
|---|---|---|
getPreferencesSync(context, options) |
实例创建 | 同步获取 Preferences 实例 |
putSync(key, value) |
写入 | 同步写入一个键值对 |
getSync(key, defaultValue) |
读取 | 同步读取,键不存在时返回默认值 |
hasSync(key) |
检查 | 判断键是否存在 |
deleteSync(key) |
删除 | 删除单个键值对 |
flushSync() |
持久化 | 将内存中的数据刷写到磁盘 |
clearSync() |
清空 | 删除所有键值对 |
on('change', callback) |
监听 | 订阅数据变更事件 |
每个方法都有三态变体:Sync(同步,直接返回值)、Promise(返回 Promise<T>)、Callback(通过回调参数返回)。本文使用 Sync 系列——代码更简洁,适合 aboutToAppear 等初始化阶段。
导入与实例创建
import preferences from '@ohos.data.preferences';
// 在 aboutToAppear 或初始化方法中
let context = getContext(this);
this.prefs = preferences.getPreferencesSync(context, { name: 'draft_notes_store' });
Options 中的 name 参数指定了存储文件的名称。系统会在应用沙箱中创建对应的 XML 文件。多个 Preferences 实例可以使用不同的 name 来隔离数据——例如 'user_settings' 和 'draft_store' 分别管理设置和草稿。
数据类型限制
Preferences 支持的值类型(ValueType)包括:number | string | boolean | Array<number> | Array<string> | Array<boolean> | Uint8Array | object | bigint。
但对于复杂对象(如 DraftItem 数组),不能直接存储——需要先序列化为 JSON 字符串。读取时再反序列化。这是本文实现的核心约束之一。
键长度与值大小限制
- 键最大长度:80 字符
- 值最大大小:8192 字符
对于草稿箱场景,8192 字符足以覆盖绝大多数笔记内容。如果需要存储更大的内容(如长文),应考虑使用文件存储或数据库。
草稿箱设计概览
功能需求
- 多草稿管理:用户可以创建多个草稿,在草稿间切换
- 自动保存:编辑时自动保存,2 秒防抖避免频繁写入
- 跨会话持久化:应用退出后草稿不丢失,重新打开恢复到最后编辑的草稿
- 标题与正文:每个草稿有独立的标题和正文内容
- 删除管理:可以删除不需要的草稿,至少保留一个
数据模型
class DraftItem {
id: number; // 唯一标识
title: string; // 草稿标题
content: string; // 正文内容
updatedAt: number; // 最后修改时间戳
constructor(id: number, title: string, content: string, updatedAt: number) {
this.id = id;
this.title = title;
this.content = content;
this.updatedAt = updatedAt;
}
}
存储策略
将所有草稿序列化为一个 JSON 数组,存储在键 'drafts_data' 下。同时存储当前活跃草稿 ID 在键 'active_id' 下:
Preferences Store
├── "drafts_data" → '[{"id":1,"title":"会议纪要","content":"...","updatedAt":1720502400000}, ...]'
└── "active_id" → 1
这种"一 JSON 全存储"策略比每个草稿一个键(draft_1, draft_2…)更简洁——不需要维护草稿 ID 列表,添加和删除草稿只需修改一个 JSON 数组。

核心实现一:初始化与加载
aboutToAppear 生命周期
aboutToAppear(): void {
this.initPrefs();
}
initPrefs(): void {
try {
let context = getContext(this);
this.prefs = preferences.getPreferencesSync(context, { name: 'draft_notes_store' });
this.loadDrafts();
} catch (e) {
// preferences 不可用时降级为空草稿
}
}
aboutToAppear 是 ArkUI 组件的生命周期回调,在组件即将显示时调用。这是执行初始化的最佳时机——此时 Context 已经可用,但 UI 尚未渲染。
try-catch 包裹整个初始化逻辑是因为 getPreferencesSync 在某些异常场景下可能抛出错误(如存储空间不足)。降级策略是:如果 Preferences 不可用,后续的编辑不会自动保存,但不影响用户在界面上的操作。
数据加载与恢复
loadDrafts(): void {
if (this.prefs === null) {
// Preferences 不可用,创建空白草稿
let now = Date.now();
this.drafts = [new DraftItem(1, '新建草稿', '', now)];
this.activeId = 1;
this.nextId = 2;
return;
}
let dataStr = this.prefs.getSync('drafts_data', '') as string;
let activeId = this.prefs.getSync('active_id', 0) as number;
if (dataStr.length > 0) {
// 解析 JSON 并还原草稿列表
let raw: Record<string, number | string>[] =
JSON.parse(dataStr) as Record<string, number | string>[];
let loaded: DraftItem[] = [];
for (let i = 0; i < raw.length; i++) {
let item = raw[i];
loaded.push(new DraftItem(
item['id'] as number,
item['title'] as string,
item['content'] as string,
item['updatedAt'] as number
));
}
this.drafts = loaded;
// 恢复上次活跃的草稿
let found = false;
for (let i = 0; i < loaded.length; i++) {
if (loaded[i].id === activeId) {
this.activeId = activeId;
this.editTitle = loaded[i].title;
this.editContent = loaded[i].content;
found = true;
break;
}
}
if (!found && loaded.length > 0) {
// 上次活跃草稿被删除,回退到第一个
this.activeId = loaded[0].id;
this.editTitle = loaded[0].title;
this.editContent = loaded[0].content;
}
} else {
// 首次启动
let now = Date.now();
this.drafts = [new DraftItem(1, '新建草稿', '', now)];
this.activeId = 1;
this.editTitle = '新建草稿';
this.editContent = '';
this.saveDrafts(); // 立即保存初始状态
}
}
这段代码体现了"防御性数据恢复"的思路:每一步都有失败处理和默认值。当 JSON 解析失败、活跃 ID 对应的草稿不存在、或数据为空时,都不会导致崩溃——而是回退到一个干净的初始状态。
ArkTS 严格类型与 JSON 解析
这里有一个值得展开的技术细节。在标准 TypeScript 中,JSON.parse() 返回 any 类型,你可以直接访问 raw[i].id 而不报类型错误。但在 ArkTS 的严格模式下,any 被禁止使用(arkts-no-any-unknown 规则)。
解决方案是显式类型标注:
// 错误(ArkTS 严格模式不允许)
let raw = JSON.parse(dataStr); // raw 类型为 any
// 正确
let raw: Record<string, number | string>[] =
JSON.parse(dataStr) as Record<string, number | string>[];
Record<string, number | string> 表示"键为字符串、值为数字或字符串的对象"。因为 DraftItem 的字段恰好都是 number 和 string 类型,这个类型可以覆盖。访问时需要通过 item['fieldName'] 配合 as 断言来取得具体的值类型:
let id = item['id'] as number;
let title = item['title'] as string;
这种写法比标准 TypeScript 繁琐,但 ArkTS 的类型严格性确保了编译期的类型安全——将运行时错误(“属性不存在”、“类型不匹配”)前置到编译期发现。
核心实现二:自动保存机制
防抖调度
自动保存的关键不是"每次都写",而是"停止输入后等待一小段时间再写"。这样可以避免用户每敲一个字符就触发一次磁盘写入。
private saveTimer: number = -1;
scheduleSave(): void {
if (this.saveTimer >= 0) {
clearTimeout(this.saveTimer); // 取消之前的定时器
}
this.saveStatus = '未保存';
this.saveTimer = setTimeout(() => {
this.saveDrafts();
}, 2000); // 2 秒后执行保存
}
每次用户输入(onChange 事件触发)时调用 scheduleSave()。如果 2 秒内再次输入,之前的定时器被取消,重新计时。只有用户在 2 秒内停止输入,保存才真正执行。
这是一个经典的"防抖(debounce)"模式——在前端开发中无处不在(搜索建议、窗口 resize、表单验证),在移动端同样重要(减少 I/O 操作、节省电量)。
编辑触发
标题和正文的 onChange 都会触发自动保存调度:
TextInput({ placeholder: '草稿标题', text: this.editTitle })
.onChange((val: string) => {
this.editTitle = val;
this.scheduleSave();
})
TextArea({ placeholder: '开始写点什么...', text: this.editContent })
.onChange((val: string) => {
this.editContent = val;
this.scheduleSave();
})
不需要额外的"保存"按钮——但本文 Demo 也提供了"立即保存"按钮,用于用户想手动确保持久化的场景(如在退出应用前):
Button('立即保存')
.onClick(() => { this.saveDrafts(); })
保存执行
saveDrafts(): void {
// 1. 更新内存中的草稿列表
let updated: DraftItem[] = [];
let now = Date.now();
let found = false;
for (let i = 0; i < this.drafts.length; i++) {
if (this.drafts[i].id === this.activeId) {
updated.push(new DraftItem(this.activeId, this.editTitle, this.editContent, now));
found = true;
} else {
updated.push(this.drafts[i]);
}
}
if (!found) {
updated.push(new DraftItem(this.activeId, this.editTitle, this.editContent, now));
}
this.drafts = updated;
// 2. 序列化并写入 Preferences
if (this.prefs !== null) {
try {
let plain: object[] = [];
for (let i = 0; i < updated.length; i++) {
let d = updated[i];
let obj: Record<string, Object> = {};
obj['id'] = d.id;
obj['title'] = d.title;
obj['content'] = d.content;
obj['updatedAt'] = d.updatedAt;
plain.push(obj);
}
this.prefs.putSync('drafts_data', JSON.stringify(plain));
this.prefs.putSync('active_id', this.activeId);
this.prefs.flushSync(); // 强制刷写到磁盘
// 3. 更新保存状态显示
let date = new Date(now);
let h = date.getHours().toString().padStart(2, '0');
let m = date.getMinutes().toString().padStart(2, '0');
let s = date.getSeconds().toString().padStart(2, '0');
this.saveStatus = '已保存 ' + h + ':' + m + ':' + s;
} catch (e) {
this.saveStatus = '保存失败';
}
}
}
保存过程的三个步骤:
- 内存更新:在当前草稿列表中更新活跃草稿(或追加新草稿),遵循不可变模式创建新数组
- 持久化写入:将草稿列表序列化为 JSON,通过
putSync写入,再通过flushSync强制写到磁盘 - UI 反馈:更新时间戳,让用户知道保存已成功执行
flushSync() 是这里的关键调用。putSync 将数据写入内存缓存,但不会立即写到磁盘——系统会在合适的时机批量刷写。flushSync() 强制立即刷写,确保在应用被杀死等异常场景下数据不丢失。对于草稿这种"用户花了时间写了内容"的数据,立即刷写的开销完全可以接受。
核心实现三:草稿切换与新建
草稿切换
用户点击草稿列表中的某一项时,先保存当前草稿,再加载目标草稿:
switchDraft(id: number): void {
// 先保存当前草稿(如果正在编辑)
this.saveDrafts();
// 查找并加载目标草稿
for (let i = 0; i < this.drafts.length; i++) {
if (this.drafts[i].id === id) {
this.activeId = id;
this.editTitle = this.drafts[i].title;
this.editContent = this.drafts[i].content;
this.saveStatus = '已加载';
break;
}
}
}
先保存再切换是一个重要的设计决策。如果先切换再保存,用户刚才在旧草稿上输入的内容会丢失(因为 saveDrafts 会基于新的 activeId 覆盖错误的数据)。这个"保存-切换"的顺序确保了数据完整性。
新建草稿
newDraft(): void {
this.saveDrafts(); // 保存当前草稿
let now = Date.now();
let newItem = new DraftItem(this.nextId, '新建草稿', '', now);
let updated = this.drafts.slice();
updated.push(newItem);
this.drafts = updated;
this.activeId = this.nextId;
this.editTitle = '新建草稿';
this.editContent = '';
this.saveStatus = '新建草稿';
this.nextId = this.nextId + 1;
}
同样先保存当前草稿,然后创建新的 DraftItem。nextId 自增计数器在整个会话期间递增——它从已加载草稿的最大 ID + 1 开始,确保新草稿的 ID 永远不会与已有草稿冲突。
删除草稿
deleteDraft(id: number): void {
let updated: DraftItem[] = [];
for (let i = 0; i < this.drafts.length; i++) {
if (this.drafts[i].id !== id) {
updated.push(this.drafts[i]);
}
}
// 至少保留一个草稿
if (updated.length === 0) {
let now = Date.now();
updated.push(new DraftItem(this.nextId, '新建草稿', '', now));
this.nextId = this.nextId + 1;
}
this.drafts = updated;
// 如果删除的是活跃草稿,切换到第一个
if (id === this.activeId) {
this.activeId = updated[0].id;
this.editTitle = updated[0].title;
this.editContent = updated[0].content;
}
// 持久化
if (this.prefs !== null) {
try {
let plain: object[] = [];
for (let i = 0; i < updated.length; i++) {
let d = updated[i];
let obj: Record<string, Object> = {};
obj['id'] = d.id;
obj['title'] = d.title;
obj['content'] = d.content;
obj['updatedAt'] = d.updatedAt;
plain.push(obj);
}
this.prefs.putSync('drafts_data', JSON.stringify(plain));
this.prefs.putSync('active_id', this.activeId);
this.prefs.flushSync();
} catch (e) {
// 静默处理
}
}
this.saveStatus = '已删除';
}
删除逻辑中的关键保护措施:不允许删除最后一个草稿。如果用户试图删除最后一个,系统会静默地创建一个新的空白草稿来替代。这避免了"零草稿"状态给 UI 带来的空状态处理复杂度。
核心实现四:UI 状态反馈
保存状态指示器
标题栏右侧有一个状态徽章,根据 saveStatus 的值显示不同背景色:
.backgroundColor(
this.saveStatus.startsWith('已保存') ? '#52C41A80' : // 绿色
this.saveStatus.startsWith('保存失败') ? '#FF4D4F80' : // 红色
'#FFFFFF20' // 默认半透明白
)
三种状态的视觉编码:
- 已保存(绿色):用户输入已持久化到磁盘,数据安全
- 保存失败(红色):写入出现问题,提醒用户注意
- 未保存(半透明白):用户正在输入,尚未触发自动保存
这种"红绿灯"式的状态指示让用户对数据安全状态一目了然——无需阅读文字,扫一眼颜色即可判断。
草稿列表高亮
当前活跃的草稿在列表中通过蓝色竖线标识和浅蓝背景区分:
Row()
.width(3)
.height(36)
.borderRadius(2)
.backgroundColor(draft.id === this.activeId ? '#1677FF' : '#E8E8E8')
// 背景色
.backgroundColor(draft.id === this.activeId ? '#F8FAFF' : '#FFFFFF')
蓝色竖线是本系列 Demo 中反复出现的视觉模式——从阅读器的进度条到看板的优先级标签,再到草稿箱的选中指示器。这种视觉一致性帮助用户建立"蓝色 = 当前/活跃"的心理模型。
完整页面结构
Column
├── Header(标题 + 草稿总数 + 保存状态徽章)
├── Editor Area
│ ├── TextInput(标题输入 · onChange → scheduleSave)
│ ├── TextArea(正文编辑 · onChange → scheduleSave)
│ └── Action Bar(当前位置 + 立即保存按钮)
├── Section Header(所有草稿 + 新建按钮)
└── Scroll
└── Draft List
├── Draft Card 1(蓝色竖线 + 标题 + 预览 + 时间 · 可点击切换)
├── Draft Card 2(灰色竖线 · 可点击切换)
└── ...
常见问题与解决方案
问题一:JSON.parse 报 “arkts-no-any-unknown”
原因:ArkTS 严格模式禁止 any 类型,JSON.parse 返回 any。
解决:使用显式类型断言 as Record<string, number | string>[],并通过 item['key'] as Type 访问字段。
问题二:应用重启后草稿丢失
原因:putSync 后没有调用 flushSync,数据只写入内存缓存,应用被杀死前未刷写到磁盘。
解决:每次 putSync 后调用 flushSync() 强制持久化。
问题三:切换草稿后之前编辑的内容丢失
原因:switchDraft 中先设置了新的 editTitle 和 editContent,然后 saveDrafts 把新草稿的空白内容保存到了旧草稿上。
解决:在切换草稿前先调用 saveDrafts() 保存当前草稿状态。
问题四:频繁自动保存导致性能问题
原因:每次按键都触发 saveDrafts,大量磁盘 I/O。
解决:使用防抖(debounce)模式,2 秒无输入后才执行保存。这既保证了数据安全,又避免了不必要的写入。
进阶扩展方向
1. 云端同步
在 saveDrafts 中增加云同步逻辑——保存到 Preferences 后,同时将草稿数据发送到云端(通过 HTTP 请求或分布式数据服务)。在下一次 loadDrafts 时,先从云端拉取最新数据,与本地合并。
2. 自动保存历史版本
不光保存当前版本,每次 saveDrafts 时将旧版本追加到一个"历史"键下。用户可以通过"版本历史"功能回溯到之前的编辑状态。
3. 草稿加密
对于敏感内容(如日记、密码备忘),在 JSON.stringify 后执行加密,在 JSON.parse 前执行解密。可以使用 @ohos.security.cryptoFramework 提供的 AES 加密能力。
4. 富文本草稿
将正文从纯文本扩展为 Markdown 或富文本。内容以 HTML/Markdown 字符串形式存储,在编辑器中提供格式化工具栏,在列表中渲染为纯文本预览。
Preferences vs 数据库:何时用哪个?
这是新手最常问的问题。简单来说:
| 场景 | 推荐方案 |
|---|---|
| 用户设置(开关、选项) | Preferences |
| 草稿/临时数据 | Preferences |
| 少量结构化数据(< 50 条) | Preferences |
| 大量结构化数据(50-5000 条) | RelationalStore |
| 文件/二进制数据 | 文件存储 |
| 跨设备同步 | 分布式数据服务 |
Preferences 的优势在于简单——不需要建表、不需要写 SQL、不需要管理数据库版本迁移。劣势在于不支持复杂查询、不适合大量数据。对于草稿箱这个场景(通常 5-20 条草稿),Preferences 是完美匹配。
总结
本文通过构建一个完整的"草稿箱"应用,深入讲解了 HarmonyOS @ohos.data.preferences API 的实战应用。核心要点如下:
- Preferences 实例创建:
getPreferencesSync(context, { name })同步获取,name用于数据隔离 - CRUD 操作:
putSync/getSync/hasSync/deleteSync/flushSync覆盖读写删全流程 - JSON 序列化策略:复杂数据序列化为 JSON 字符串存储,ArkTS 严格模式下需要显式类型断言
- 防抖自动保存:2 秒 debounce 模式,
clearTimeout+setTimeout实现,兼顾体验和性能 - 数据恢复:
aboutToAppear中读取并反序列化,多层防御性检查确保不崩溃 - 草稿生命周期:创建→编辑→自动保存→切换/删除,状态管理保证不丢数据
- 类型安全:
Record<string, number | string>类型和as断言解决 ArkTS 严格模式约束
数据持久化是移动开发中"不那么酷炫但绝对不能出错"的基础能力。一个草稿不会自动保存的笔记应用,即使 UI 再精美,也无法赢得用户的信任。Preferences API 以最简洁的方式解决了这个问题——几行代码,就能给用户"我的内容不会丢"的安全感。
更多推荐

所有评论(0)