一个配置类的状态,主题、语言、字体大小,放进 PersistenceV2 里,代码写得很顺手:connect() 拿到对象,读写属性,UI 自动刷新。开发阶段一切正常,直到覆盖安装之后老用户升级上来,页面直接崩了。日志里只有一行:Cannot read property 'theme' of undefined。
PersistenceV2.connect() 的返回类型从一开始就是 T | undefined。磁盘上不存在对应 key 的数据时,它必然 返回 undefined,不管你是不是觉得"这个字段应该一直存在"。新安装、清数据、从旧版本升上来、key 改名,任何一种情况都会让你拿到 undefined。
defaultCreator 不是可选项,是唯一的安全入口
connect() 的第三个参数 defaultCreator 就是为这个时刻准备的。但很多人只写了前两个参数,或者第三个参数写了一个返回空对象的 lambda,然后在页面里直接访问 state.theme,崩在空对象上。
typescript
@ObservedV2
class AppConfig {
@Trace theme: string = 'system';
@Trace language: string = 'zh-CN';
@Trace fontScale: number = 1.0;
}
const CONFIG_KEY = 'app_config_v1';
export function getConfig(): AppConfig {
const config = PersistenceV2.connect(
AppConfig,
CONFIG_KEY,
() => new AppConfig() // 磁盘没有数据时走这里
);
if (!config) {
throw new Error('config should never be undefined with defaultCreator');
}
return config;
}
() => new AppConfig() 保证了磁盘没有数据时至少有一个带默认字段的实例返回。config 后面的判空不是为了防御性编程,是为了让类型收窄到 AppConfig,编译器不会在后续访问 config.theme 时报错。
旧版本升级的反序列化不走构造函数
defaultCreator 解决的是"磁盘上什么都没有"的场景。从旧版本升级上来是另一种情况:磁盘上有序列化过的 JSON,PersistenceV2 把它反序列化成对象,但不会走类的构造函数 。你在类里写的 theme: string = 'system' 这类字段默认值,在反序列化路径上不生效。旧版本 JSON 里没有的新字段,拿到手就是 undefined。
所以 connect() 之后、UI 使用之前,需要一个显式的字段补全和版本迁移步骤。不要在页面 aboutToAppear 里零散地补,放在 getConfig() 返回之前,集中做一次。
typescript
function migrateConfig(config: AppConfig): void {
if (config.theme === undefined) config.theme = 'system';
if (config.fontScale === undefined) config.fontScale = 1.0;
// 读取存储的 version 字段,按版本号逐级迁移
}
undefined 判断是迁移的核心。旧 JSON 里缺哪个字段,这里就补哪个。迁移过程中如果写入失败,旧数据仍然在磁盘上,下一次 connect() 还会读到它,补全逻辑会再跑一遍。不要在迁移失败时清掉存储,那才是把用户数据弄丢的方式。
PersistenceV2 不解决多 Ability 并发写
PersistenceV2 的定位是持久化,不是并发控制。同一进程内多个 UIAbility 各自 connect() 同一个 key,拿到的是共享引用,各自基于旧快照修改不同字段再写回,后写覆盖先写是预期行为。主题和未读数如果在不同 Ability 里独立修改,最终状态取决于写入顺序,不是"最后那个业务上应该赢的"。
把写入收口到一个 Manager 里,字段级更新而不是整对象替换,是比换存储方案更直接的做法。如果业务上需要"主题和未读数同帧展示",那就必须让它们来自同一个源状态,而不是两个独立 connect 的 key 在 UI 上拼在一起。持久化解决"重启之后还在不在",不解决"谁先写谁后写"。