这次处理版本升级后的配置问题。

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)、Schema 2→3、MIGRATED、Migrated Keys 6、Rollback Count 1、Pages Synced 3、Theme system、Font Scale 1.15、Last Migration 10:54:32。

没有 schemaVersion 时,“某个字段有没有”不能说明用户从哪个版本升级上来,所以明确保存:

schemaVersion = 3
migrationState = MIGRATED

以后升级走显式版本路径。

二、迁移前先做快照

第一段代码解决中途失败。

import { preferences } from '@kit.ArkData'

private async createSnapshot(
  prefs: preferences.Preferences,
  snapshotId: string
): Promise<Record<string, preferences.ValueType>> {
  const keys: string[] = [
    'schemaVersion', 'theme',
    'fontScalePct', 'fontScale',
    'compactMode', 'migrationState'
  ]

  const snapshot: Record<string, 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
): Promise<void> {
  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 Count=1。

四、回滚要恢复旧值,也要恢复“原来没有”

我之前踩过的坑是:只把 fontScalePct 写回去,却忘了删除已经创建的 fontScale。下一次启动时两套字段同时存在,代码到底读哪个又成了新问题。

所以 restoreSnapshot() 会遍历快照:普通值用 put() 恢复,值为 __MISSING__ 的字段执行 delete();随后把 rollbackCount 加 1、把 migrationState 写成 ROLLED_BACK,最后统一 flush()。这一步的重点不是“恢复几个数字”,而是把整个 Schema 恢复到迁移前可解释的状态。

五、只有 MIGRATED 以后才同步 AppStorage

Preferences 是持久化配置,AppStorage 是页面运行态。迁移进行到一半时,不应该交替更新两边。

private async publishRuntimeState(
  prefs: preferences.Preferences
): Promise<void> {
  const theme =
    String(await prefs.get('theme', 'system'))
  const fontScale =
    Number(await prefs.get('fontScale', 1.0))

  AppStorage.setOrCreate<string>('theme', theme)
  AppStorage.setOrCreate<number>('fontScale', fontScale)
  AppStorage.setOrCreate<string>(
    'settingsSchemaState', 'MIGRATED'
  )

  this.pagesSynced = 3
}

首页、列表页和“我的”页都订阅同一份运行态,最终三个页面统一为 theme=system、fontScale=1.15。迁移成功后由桥接层一次性发布,页面只订阅 AppStorage。

六、日志必须能复原迁移路径

工程中 EntryAbility 只负责启动迁移,SettingsMigrationService 负责版本判断、验证和回滚,SettingsStateBridge 只在成功后同步 AppStorage。

HiLog 固定保留:

schema 2 -> 3
snapshot=cfg_20261001_11
migratedKeys=6
rollbackCount=1
pagesSynced=3
State: MIGRATING -> MIGRATED

线上出现问题时,先看 schema 路径和 snapshotId。

七、运行结果要看页面是否收敛

最终手机界面显示:cfg_20261001_11、3.2.0(30200)、2→3、MIGRATED、Migrated Keys=6、Rollback Count=1、Pages Synced=3、Theme=system、Font Scale=1.15、Last Migration=10:54:32。

下方三个状态卡代表首页、列表页和“我的”页,都显示 SYNCED。

Schema 升级真正结束的条件,是持久层通过验证、运行态已发布、页面也完成收敛。

八、重复启动和跨版本升级必须幂等

schema 已经是 3 时直接退出,不能每次启动都重复创建快照。

如果用户从 schema 1 直接升级到 3,我会按 1→2→3 分步执行,每一步独立验证和回滚。

迁移中应用被终止时,下次启动根据 migrationState=MIGRATING 恢复快照或重新执行。

九、配置很轻,升级过程却不能靠运气

流程固定为:

SNAPSHOT → MIGRATING → VALIDATING → MIGRATED

失败路径:

MIGRATING → ERROR → ROLLED_BACK → RETRY

页面同步只发生在 MIGRATED 之后。

Preferences 不复杂,真正需要设计的是升级中的时间顺序。把持久化迁移和运行态发布拆开后,就不会因为某个页面先启动或一次异常而留下半套新数据。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐