HarmonyOS 应用开发《掌上英语》第30篇:数据序列化与类型安全——ArkTS 中的 JSON 处理
数据序列化与类型安全——ArkTS 中的 JSON 处理

引言
在 HarmonyOS 应用开发中,Preferences 键值存储本质上只能存储基本类型(string、number、boolean)和可序列化的对象。当我们需要存储复杂结构(如对象数组、嵌套对象)时,序列化与反序列化就成为了一个绕不开的话题。ArkTS 作为 TypeScript 的超集,提供了 JSON.stringify 和 JSON.parse 等熟悉的工具,但在实际使用中存在许多值得注意的陷阱。本文将以项目中的实际代码为例,深入分析 ArkTS 中数据序列化和类型安全的实践要点。
一、隐式序列化:PreferenceUtil 的自动处理
在项目的 Manager 层,写入数据时通常不显式调用 JSON.stringify。这是因为 PreferenceUtil.put 方法接受的 value 类型是 preferences.ValueType:
// NewWordManager 写入生词列表
private saveNewWords(words: NewWordItem[]): void {
PreferenceUtil.getInstance().put(this.NEW_WORD_KEY, words);
}
// StatisticsManager 写入统计数据
PreferenceUtil.getInstance().put(this.STATISTICS_KEY, mergedStats);
重要发现:preferences.ValueType 在 ArkTS 中支持 Object 和 Array 类型,Preferences API 内部会处理序列化/反序列化。这意味着 words 数组或 mergedStats 对象会被自动序列化为 JSON 字符串并存储。
读取时同样自动反序列化:
// 读取生词列表
let words = PreferenceUtil.getInstance().get(this.NEW_WORD_KEY, []) as NewWordItem[];
// 读取统计数据
let stats = PreferenceUtil.getInstance().get(this.STATISTICS_KEY, undefined) as PracticeStatistics;
二、显式序列化的应用场景
虽然 Preferences API 支持自动序列化,但在某些场景下,显式序列化仍然有必要:
2.1 复杂嵌套结构的深度复制
// 学习计划克隆 - 使用手动深度复制而非 JSON 序列化
private cloneLearningPlan(plan: LearningPlan): LearningPlan {
return {
dailyWordGoal: plan.dailyWordGoal,
dailyListeningGoal: plan.dailyListeningGoal,
// ... 逐字段复制
badges: plan.badges.concat() // 数组需要 .concat() 深复制
};
}
为什么不直接用 JSON.parse(JSON.stringify(plan))?因为 Date 对象在 JSON 序列化中会变成字符串,反序列化后不再是 Date 类型。
2.2 部分更新时的空值合并
在 LearningPlanManager 的部分更新中,?? 空值合并运算符扮演了关键角色:
private mergeLearningPlans(currentPlan: LearningPlan, plan: Partial<LearningPlan>): LearningPlan {
return {
dailyWordGoal: plan.dailyWordGoal ?? currentPlan.dailyWordGoal,
dailyListeningGoal: plan.dailyListeningGoal ?? currentPlan.dailyListeningGoal,
// ...
badges: plan.badges ? plan.badges.concat() : currentPlan.badges.concat()
};
}
为什么使用 ?? 而非 ||?
??只在左侧为null或undefined时取右侧值。||在左侧为false、0、''等 falsy 值时也会取右侧值。- 对于
dailyWordGoal: 0这样的有效值,??可以正确处理,而||会错误地取默认值。
三、反序列化后的类型安全
3.1 as 类型断言的局限性
在 ArkTS 中,as 关键字只是编译时类型断言,不会在运行时做类型校验:
// 编译时通过,但运行时可能不是 NewWordItem[]
let words = PreferenceUtil.getInstance().get(this.NEW_WORD_KEY, []) as NewWordItem[];
如果存储的数据格式被意外修改(如版本升级改变了数据结构),as 断言不会抛出任何错误,直到代码访问不存在的属性时才会出现运行时错误。
3.2 运行时安全性检查
更安全的做法是在反序列化后做校验,或者提供默认值兜底:
// StatisticsManager 中的安全读取
public getHistoryStatistics(): PracticeStatistics {
try {
let stats = PreferenceUtil.getInstance().get(this.STATISTICS_KEY, undefined) as PracticeStatistics;
if (!stats) {
return this.getEmptyStatistics(); // 返回空统计而非 null
}
return stats;
} catch (e) {
Logger.error('StatisticsManager', `获取历史统计数据失败: ${JSON.stringify(e)}`);
return this.getEmptyStatistics();
}
}
3.3 undefined 作为默认值标识
当需要区分"未设置"和"空数组"时,使用 undefined 作为默认值:
// 学习计划 - undefined 表示还未设置过
let plan = PreferenceUtil.getInstance().get(this.LEARNING_PLAN_KEY, undefined) as LearningPlan;
if (!plan) {
plan = this.cloneLearningPlan(DEFAULT_LEARNING_PLAN);
this.saveLearningPlan(plan);
}
// 生词本 - 空数组 [] 是有效值
let words = PreferenceUtil.getInstance().get(this.NEW_WORD_KEY, []) as NewWordItem[];
四、日期对象的序列化陷阱
日期对象的序列化是 JSON 处理中最常见的陷阱之一:
let date = new Date('2025-01-15');
let json = JSON.stringify(date);
console.log(json); // '"2025-01-15T00:00:00.000Z"' —— 变成了字符串
let parsed = JSON.parse(json);
console.log(parsed instanceof Date); // false —— 不再是 Date 对象
在项目中,正确的做法是将日期存储为时间戳(number),而不是 Date 对象:
// 存储时间戳(推荐)
addTime: Date.now() // number 类型,安全可序列化
// 读取后转换为 Date
let addDate = new Date(wordItem.addTime);
// 连续天数比较中使用时间戳
let today = new Date();
today.setHours(0, 0, 0, 0);
let todayTimestamp = today.getTime(); // 存储的是 number
最佳实践:所有需要序列化的日期值都使用 number 时间戳,而非 Date 对象。
五、版本兼容与默认字段
当应用版本升级、数据模型新增字段时,旧数据中会缺少新字段。通过 ?? 和默认值可以优雅地处理版本兼容:
// 假设 LearningPlan 在新版本中新增了 reminderEnabled 字段
// 旧数据中没有这个字段,读取后为 undefined
// 使用 ?? 可以提供默认值
private mergeLearningPlans(currentPlan: LearningPlan, plan: Partial<LearningPlan>): LearningPlan {
return {
reminderEnabled: plan.reminderEnabled ?? currentPlan.reminderEnabled, // undefined 时保留旧值
// ...
};
}
// 更好的方式是提供 DEFAULT 常量
export const DEFAULT_LEARNING_PLAN: LearningPlan = {
reminderEnabled: false, // 新字段默认值
reminderTime: '08:00', // 新字段默认值
// ...
};
六、最佳实践
6.1 统一的序列化入口
通过 PreferenceUtil 封装所有的序列化逻辑,避免在各 Manager 中散落 JSON.stringify 调用。
6.2 避免引用共享
反序列化的对象与原始对象没有引用关系,这是安全的。但在 Manager 内部操作时应避免直接修改反序列化的结果:
// ❌ 直接修改反序列化的引用
let plan = this.getLearningPlan();
plan.todayWordCount += count; // 修改了内存中的对象
this.saveLearningPlan(plan); // 序列化后写入
// ✅ 读取后立即写入是安全的(读取-修改-写回模式)
6.3 日志序列化
在 Logger 中记录复杂对象时,记得使用 JSON.stringify:
Logger.info('PreferenceUtil', `put: ${key} = ${JSON.stringify(value)}`);
如果直接传入对象,Logger 可能输出 [Object object],无法看到具体内容。
七、总结
ArkTS 中的 JSON 序列化处理需要开发者注意以下几点:
- 隐式序列化:Preferences API 对
Object/Array类型自动处理序列化,Manager 层无需显式调用JSON.stringify。 - 类型断言:
as关键字仅有编译时作用,运行时需要通过默认值和空值检查确保安全。 - 日期处理:永远使用时间戳(
number)而非Date对象进行持久化。 - 版本兼容:通过
??运算符和默认值常量(DEFAULT_LEARNING_PLAN)处理数据模型升级。 - 引用安全:采用"读取-修改-写回"模式,避免意外修改共享引用。
理解这些要点后,开发者可以在 ArkTS 中编写出既类型安全又健壮的数据持久化代码。
更多推荐


所有评论(0)