这次处理版本升级后的配置问题。SettingsMigrationLab在 3.1.x 里把字号保存成整数百分比例如100、115到了 3.2.0我想统一成1.0、1.15同时补上theme、compactMode等字段。第一次实现时我直接读旧值、覆盖新值。测试中途故意抛异常后首页已经读到新字段列表页仍拿着旧 AppStorage 值Preferences 里也只改了一半。应用能启动却处在“表面正常、配置已经分叉”的状态。这篇只解决一个问题Schema 从 2 升级到 3 时怎样把迁移拆成可以验证、可以回滚并且一次性同步到多个页面。一、先给配置一个明确版本最终数据固定为cfg_20261001_11、3.2.0(30200)、Schema2→3、MIGRATED、Migrated Keys6、Rollback Count1、Pages Synced3、Themesystem、Font Scale1.15、Last Migration10:54:32。没有schemaVersion时“某个字段有没有”不能说明用户从哪个版本升级上来所以明确保存schemaVersion 3 migrationState MIGRATED以后升级走显式版本路径。二、迁移前先做快照第一段代码解决中途失败。import { preferences } from kit.ArkData private async createSnapshot( prefs: preferences.Preferences, snapshotId: string ): PromiseRecordstring, preferences.ValueType { const keys: string[] [ schemaVersion, theme, fontScalePct, fontScale, compactMode, migrationState ] const snapshot: Recordstring, preferences.ValueType {} for (const key of keys) { snapshot[key] await prefs.get(key, __MISSING__) } await prefs.put( snapshot:${snapshotId}, JSON.stringify(snapshot) ) await prefs.flush() return snapshot }我只保存这次会改到的字段。__MISSING__表示旧版本没有这个 key回滚时应该删除。三、迁移、验证、版本落盘必须有顺序2→3 迁移里schemaVersion最后才写。private async migrateV2ToV3( prefs: preferences.Preferences ): Promisevoid { await prefs.put(migrationState, MIGRATING) await prefs.flush() const oldPct Number(await prefs.get(fontScalePct, 100)) const fontScale Number((oldPct / 100).toFixed(2)) await prefs.put(fontScale, fontScale) await prefs.put( theme, await prefs.get(theme, system) ) await prefs.put( compactMode, await prefs.get(compactMode, false) ) await prefs.delete(fontScalePct) const theme String(await prefs.get(theme, system)) if (fontScale 0.8 || fontScale 1.6) { throw new Error(invalid fontScale: ${fontScale}) } if (![system, light, dark].includes(theme)) { throw new Error(invalid theme: ${theme}) } await prefs.put(schemaVersion, 3) await prefs.put(migrationState, MIGRATED) await prefs.flush() }如果开头就把schemaVersion写成 3后面失败下次启动可能误以为升级结束。本次字号115最终转成1.15主题没有用户值时使用system旧字段fontScalePct在验证通过后删除。Demo 第一次校验故意失败因此最终Rollback Count1。四、回滚要恢复旧值也要恢复“原来没有”我之前踩过的坑是只把fontScalePct写回去却忘了删除已经创建的fontScale。下一次启动时两套字段同时存在代码到底读哪个又成了新问题。所以restoreSnapshot()会遍历快照普通值用put()恢复值为__MISSING__的字段执行delete()随后把rollbackCount加 1、把migrationState写成ROLLED_BACK最后统一flush()。这一步的重点不是“恢复几个数字”而是把整个 Schema 恢复到迁移前可解释的状态。五、只有 MIGRATED 以后才同步 AppStoragePreferences 是持久化配置AppStorage 是页面运行态。迁移进行到一半时不应该交替更新两边。private async publishRuntimeState( prefs: preferences.Preferences ): Promisevoid { const theme String(await prefs.get(theme, system)) const fontScale Number(await prefs.get(fontScale, 1.0)) AppStorage.setOrCreatestring(theme, theme) AppStorage.setOrCreatenumber(fontScale, fontScale) AppStorage.setOrCreatestring( settingsSchemaState, MIGRATED ) this.pagesSynced 3 }首页、列表页和“我的”页都订阅同一份运行态最终三个页面统一为themesystem、fontScale1.15。迁移成功后由桥接层一次性发布页面只订阅 AppStorage。六、日志必须能复原迁移路径工程中EntryAbility只负责启动迁移SettingsMigrationService负责版本判断、验证和回滚SettingsStateBridge只在成功后同步 AppStorage。HiLog 固定保留schema 2 - 3 snapshotcfg_20261001_11 migratedKeys6 rollbackCount1 pagesSynced3 State: MIGRATING - MIGRATED线上出现问题时先看 schema 路径和 snapshotId。七、运行结果要看页面是否收敛最终手机界面显示cfg_20261001_11、3.2.0(30200)、2→3、MIGRATED、Migrated Keys6、Rollback Count1、Pages Synced3、Themesystem、Font Scale1.15、Last Migration10:54:32。下方三个状态卡代表首页、列表页和“我的”页都显示SYNCED。Schema 升级真正结束的条件是持久层通过验证、运行态已发布、页面也完成收敛。八、重复启动和跨版本升级必须幂等schema 已经是 3 时直接退出不能每次启动都重复创建快照。如果用户从 schema 1 直接升级到 3我会按1→2→3分步执行每一步独立验证和回滚。迁移中应用被终止时下次启动根据migrationStateMIGRATING恢复快照或重新执行。九、配置很轻升级过程却不能靠运气流程固定为SNAPSHOT → MIGRATING → VALIDATING → MIGRATED失败路径MIGRATING → ERROR → ROLLED_BACK → RETRY页面同步只发生在MIGRATED之后。Preferences 不复杂真正需要设计的是升级中的时间顺序。把持久化迁移和运行态发布拆开后就不会因为某个页面先启动或一次异常而留下半套新数据。