【三国志 App 实战系列 12】HarmonyOS BackupExtensionAbility 实战:备份配置、恢复入口与本地数据边界
当前应用已上架鸿蒙应用商店,搜索《耳畔三国·将星落》下载,欢迎各位看官尝鲜、吐槽!拜谢!
系列第 12 篇。前一篇讲 AppGallery 评论入口,这一篇继续处理发布阶段容易被忽略的能力:应用已经有收藏、笔记、主题和听书进度,那么这些本地数据要不要跟随系统备份恢复?

一、为什么本地优先应用更需要备份意识
《耳畔三国·将星落》是一个本地优先的三国知识应用。人物、事件、文章、地图这些模板内容随安装包分发;收藏、笔记、主题偏好、听书进度则由用户在使用过程中产生。
本地优先有明显优势:
- 首屏稳定,不依赖网络接口。
- 审核和隐私风险低。
- 内容模型简单,首版容易闭环。
- 用户笔记、收藏和听书进度都在本机快速读写。
但它也带来一个问题:如果用户换机、恢复系统或重新安装应用,哪些数据应该恢复?如果完全不考虑备份,用户的个人笔记和听书进度就可能丢失;如果把所有内容都塞进备份,又会浪费空间,还可能把模板数据和用户数据混在一起。

二、本文目标与边界
这一篇不做复杂云同步,只讲 HarmonyOS 应用内的最小备份闭环:
- 在
module.json5中声明备份扩展能力。 - 创建
EntryBackupAbility。 - 配置
backup_config.json允许备份恢复。 - 明确模板内容和用户数据的边界。
- 给出调试和验收清单。
这个能力和第 5 篇 Preferences 持久化不同。第 5 篇解决的是“应用运行期间怎么保存本地状态”;这一篇解决的是“系统迁移或恢复时,应用是否具备进入备份恢复流程的能力”。
三、模块配置:extensionAbilities
先看 entry/src/main/module.json5 中的配置:
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
这里有几个点值得注意。
第一,type 是 backup,说明它不是普通 UI Ability,也不是页面路由,而是系统在备份恢复流程中识别的扩展能力。
第二,exported 设置为 false。备份扩展不应该作为外部随意拉起的入口,它服务的是系统能力。
第三,metadata 指向 $profile:backup_config。这意味着具体是否允许备份恢复,还要看资源配置文件。
四、备份配置:backup_config.json
当前项目的配置很轻:
{
"allowToBackupRestore": true
}
这类配置容易被忽略,但它是能力开关。对于本项目来说,允许备份恢复是合理的,因为应用内存在明确的用户生成数据:
- 收藏列表。
- 阅读笔记。
- 听书队列和播放进度。
- 主题偏好。
- 是否已经评论过应用。
这些数据都不大,但很有用户价值。尤其是笔记和听书进度,一旦丢失,用户会明显感知到。
五、扩展入口:EntryBackupAbility
当前扩展实现如下:
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
const DOMAIN = 0x0000;
export default class EntryBackupAbility extends BackupExtensionAbility {
async onBackup() {
hilog.info(DOMAIN, 'testTag', 'onBackup ok');
await Promise.resolve();
}
async onRestore(bundleVersion: BundleVersion) {
hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
await Promise.resolve();
}
}
这是一种最小闭环写法:先把扩展入口、配置和日志打通,确认系统能够识别备份恢复能力,再逐步扩展更细的迁移逻辑。
对于当前项目,用户数据主要通过 Preferences 写入,字段集中在:
const PREF_FAVORITES: string = 'favorites_json';
const PREF_NOTES: string = 'notes_json';
const PREF_AUDIOS: string = 'audios_json';
const PREF_THEME_MODE: string = 'theme_mode';
const PREF_HAS_COMMENTED: string = 'has_commented';
这也让备份边界更清晰:模板内容不用备份,Preferences 中的用户状态才是重点。
六、模板数据和用户数据必须分开
本项目的数据可以分成两类:
| 类型 | 示例 | 是否应备份 | 原因 |
|---|---|---|---|
| 模板内容 | 人物、事件、文章、地图资源 | 不需要 | 随安装包分发,可重新获得 |
| 用户收藏 | 收藏的人物、事件、地图标记 | 需要 | 用户行为沉淀 |
| 用户笔记 | 阅读想法和摘录 | 需要 | 用户原创内容,价值最高 |
| 听书进度 | 已听秒数、当前队列 | 建议备份 | 恢复后能继续使用 |
| 主题偏好 | light/dark/system | 可备份 | 体积小,恢复体验更完整 |
| 评论状态 | hasCommented | 可备份 | 减少重复打扰 |
这个边界非常重要。很多应用一开始没有拆清楚“内容模板”和“用户状态”,后来做备份、导入、升级时就会出现两类问题:
- 模板数据被错误备份,恢复后与新版本内置数据冲突。
- 用户数据没有版本兼容,旧字段恢复后页面空白或解析失败。
七、恢复后要考虑字段兼容
备份恢复不是简单地“把旧文件搬回来”。如果新版本增加字段,旧备份里没有这个字段,页面仍然要能正常运行。
项目在恢复 Preferences 时采用了重新构造模型对象的方式:
const items: NoteJson[] = JSON.parse(raw) as NoteJson[];
this.noteRecords = items.map((item: NoteJson) => new NoteRecord(
item.id,
item.targetId,
item.targetType,
item.title,
item.content,
item.createdAt,
item.updatedAt
));
这比直接把 JSON 对象塞回状态数组更稳。原因是:
- 可以补默认值。
- 可以过滤异常字段。
- 可以保持页面使用的模型类型一致。
- 后续扩展字段时更容易做兼容迁移。
如果未来 NoteRecord 增加 tags 或 sourceTitle,恢复逻辑里就应该给旧数据补默认值,而不是假设所有备份都来自最新版本。
八、为什么不要把模板内容纳入备份
本项目里人物、事件、地图和专题文章都是随安装包发布的模板内容。它们位于源码和资源目录中,应用升级时会随着新包一起更新。如果把这些模板内容也当成用户数据备份,后续会出现很隐蔽的问题。
第一类问题是版本覆盖。假设 1.0.2 版本内置了 10 位人物,1.0.3 版本新增了 4 位人物。如果系统恢复时把旧模板目录覆盖回来,新版本内容反而可能丢失。用户看到的现象就是“升级后新增内容没有出现”。
第二类问题是重复合并。如果恢复逻辑没有覆盖,而是把旧模板追加到新模板,就可能出现重复人物、重复事件和重复音频条目。对于内容型 App 来说,重复数据会影响搜索、收藏和听书匹配。
第三类问题是体积浪费。地图图片、人物头像、事件插图本来就在安装包里,备份一次没有意义。真正值得备份的是用户自己产生的轻量状态。
所以更稳的做法是把备份对象收敛成一份清单:
interface BackupBoundary {
templateData: 'packaged-with-app';
userFavorites: 'backup';
userNotes: 'backup';
audioProgress: 'backup';
themePreference: 'backup';
marketFeedbackState: 'backup';
}
这段接口不是项目里的真实代码,而是用来表达边界。写文章时我喜欢把这种“工程规则”显式写出来,因为它比单纯贴一段 onBackup() 更能帮助读者理解设计选择。
九、调试命令与日志观察
备份恢复能力不像普通页面按钮那样直观,所以至少要确认配置和入口都存在:
rg -n "EntryBackupAbility|ohos.extension.backup|backup_config" entry/src/main
rg -n "allowToBackupRestore" entry/src/main/resources/base/profile
构建时也要看是否能通过:
$env:DEVECO_SDK_HOME = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk'
$env:Path = 'D:\HuaweiDevelopFormalStudy\DevEco Studio\jbr\bin;D:\HuaweiDevelopFormalStudy\DevEco Studio\sdk\default\openharmony\toolchains;D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\node;' + $env:Path
& 'D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\hvigor\bin\hvigorw.bat' assembleHap --mode module -p product=default --no-daemon
如果要观察扩展日志,可以关注:
hdc shell hilog | Select-String -Pattern "onBackup ok|onRestore ok|EntryBackupAbility"
真机完整备份恢复流程通常还依赖系统环境和账号状态,因此文章里不把“模拟器预览”当成最终验收,而是把构建、配置、入口和日志作为第一阶段检查。
十、常见问题复盘
| 问题 | 可能原因 | 处理方式 |
|---|---|---|
| 配了类但系统不识别 | module.json5 没有声明 extensionAbility |
检查 type: "backup" 和 metadata |
| 配置文件找不到 | $profile:backup_config 路径或文件名不一致 |
确认资源目录和 profile 名称 |
| 恢复后页面空白 | 旧 JSON 字段和新模型不兼容 | 恢复时重新构造模型并补默认值 |
| 模板数据重复 | 把内置内容也当用户数据备份 | 明确模板内容随安装包发布 |
| 用户笔记丢失 | 没有把用户状态纳入备份恢复边界 | 把笔记、收藏、听书进度列为核心用户数据 |
备份能力最容易被误解成“系统会自动帮我处理一切”。更稳的工程思路是:先把系统入口接好,再把数据边界和版本兼容写清楚。
十一、工程实现与验收清单
| 验收项 | 预期结果 |
|---|---|
module.json5 声明 |
存在 EntryBackupAbility,类型为 backup |
| metadata 配置 | 指向 $profile:backup_config |
| profile 文件 | allowToBackupRestore 为 true |
| 扩展类 | 继承 BackupExtensionAbility |
| 备份回调 | onBackup() 可记录日志 |
| 恢复回调 | onRestore(bundleVersion) 可记录版本信息 |
| 用户数据边界 | 收藏、笔记、听书进度、偏好被列为应恢复数据 |
| 模板数据边界 | 人物、事件、地图资源不重复备份 |
发布前我会额外确认:
rg -n "PREF_FAVORITES|PREF_NOTES|PREF_AUDIOS|PREF_THEME_MODE|PREF_HAS_COMMENTED" library2/src/main/ets/pages/MainFrame.ets
rg -n "BackupExtensionAbility|BundleVersion" entry/src/main/ets/entrybackupability
这些检查不能替代真机备份恢复,但可以保证工程结构没有缺口。
十二、如果后续要做更完整的恢复
当前项目是最小闭环:扩展入口存在,配置允许备份,用户数据边界明确。后续如果要继续增强,可以沿着三个方向推进。
第一,给用户数据增加版本号。比如在 Preferences 里额外保存 schema_version,恢复时按版本做迁移。这样旧版本笔记字段变化时,恢复逻辑可以明确知道要补哪些默认值。
interface UserDataSnapshot {
schemaVersion: number;
favoritesJson: string;
notesJson: string;
audiosJson: string;
themeMode: string;
}
第二,给恢复过程增加数据校验。收藏里的 targetId 必须能在当前模板目录中找到;找不到时不要直接丢弃,可以放进“待修复”列表,或者在 UI 上展示为“历史内容已更新”。
第三,给关键数据准备导出入口。系统备份是一层保障,手动导出笔记则是另一层保障。对于历史知识类 App 来说,用户笔记很可能比收藏列表更有价值。
这些增强不一定要在首版实现,但在设计备份边界时提前想清楚,后面就不会把迁移问题变成临时抢修。
十三、小结
BackupExtensionAbility 的价值不在于代码有多长,而在于它迫使我们回答一个产品问题:用户真正创造的数据是什么?
对这个三国志应用来说,人物、事件、文章和地图是模板内容;收藏、笔记、听书进度和偏好才是用户状态。只要这个边界清楚,备份恢复、版本升级、导入导出都会更容易设计。
本篇的最小闭环是:
module.json5声明备份扩展。backup_config.json打开备份恢复。EntryBackupAbility接住onBackup()和onRestore()。- Preferences 恢复时重新构造模型,避免旧数据直接污染页面状态。
- 用调试命令和验收表确认能力存在。
下一篇会继续沿着发布质量往回看一层:Stage 模型中的前后台生命周期如何通过 AppStorage 和 @StorageLink 传到页面,并驱动听书状态恢复。
更多推荐


所有评论(0)