当前应用已上架鸿蒙应用商店,搜索《耳畔三国·将星落》下载,欢迎各位看官尝鲜、吐槽!拜谢!

系列第 12 篇。前一篇讲 AppGallery 评论入口,这一篇继续处理发布阶段容易被忽略的能力:应用已经有收藏、笔记、主题和听书进度,那么这些本地数据要不要跟随系统备份恢复?

收藏笔记页

一、为什么本地优先应用更需要备份意识

《耳畔三国·将星落》是一个本地优先的三国知识应用。人物、事件、文章、地图这些模板内容随安装包分发;收藏、笔记、主题偏好、听书进度则由用户在使用过程中产生。

本地优先有明显优势:

  • 首屏稳定,不依赖网络接口。
  • 审核和隐私风险低。
  • 内容模型简单,首版容易闭环。
  • 用户笔记、收藏和听书进度都在本机快速读写。

但它也带来一个问题:如果用户换机、恢复系统或重新安装应用,哪些数据应该恢复?如果完全不考虑备份,用户的个人笔记和听书进度就可能丢失;如果把所有内容都塞进备份,又会浪费空间,还可能把模板数据和用户数据混在一起。

听书页进度状态

二、本文目标与边界

这一篇不做复杂云同步,只讲 HarmonyOS 应用内的最小备份闭环:

  1. module.json5 中声明备份扩展能力。
  2. 创建 EntryBackupAbility
  3. 配置 backup_config.json 允许备份恢复。
  4. 明确模板内容和用户数据的边界。
  5. 给出调试和验收清单。

这个能力和第 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"
      }
    ],
  }
]

这里有几个点值得注意。

第一,typebackup,说明它不是普通 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 对象塞回状态数组更稳。原因是:

  1. 可以补默认值。
  2. 可以过滤异常字段。
  3. 可以保持页面使用的模型类型一致。
  4. 后续扩展字段时更容易做兼容迁移。

如果未来 NoteRecord 增加 tagssourceTitle,恢复逻辑里就应该给旧数据补默认值,而不是假设所有备份都来自最新版本。

八、为什么不要把模板内容纳入备份

本项目里人物、事件、地图和专题文章都是随安装包发布的模板内容。它们位于源码和资源目录中,应用升级时会随着新包一起更新。如果把这些模板内容也当成用户数据备份,后续会出现很隐蔽的问题。

第一类问题是版本覆盖。假设 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 文件 allowToBackupRestoretrue
扩展类 继承 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 的价值不在于代码有多长,而在于它迫使我们回答一个产品问题:用户真正创造的数据是什么?

对这个三国志应用来说,人物、事件、文章和地图是模板内容;收藏、笔记、听书进度和偏好才是用户状态。只要这个边界清楚,备份恢复、版本升级、导入导出都会更容易设计。

本篇的最小闭环是:

  1. module.json5 声明备份扩展。
  2. backup_config.json 打开备份恢复。
  3. EntryBackupAbility 接住 onBackup()onRestore()
  4. Preferences 恢复时重新构造模型,避免旧数据直接污染页面状态。
  5. 用调试命令和验收表确认能力存在。

下一篇会继续沿着发布质量往回看一层:Stage 模型中的前后台生命周期如何通过 AppStorage@StorageLink 传到页面,并驱动听书状态恢复。

Logo

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

更多推荐