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 写入生词列表privatesaveNewWords(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 字符串并存储。读取时同样自动反序列化// 读取生词列表letwordsPreferenceUtil.getInstance().get(this.NEW_WORD_KEY,[])asNewWordItem[];// 读取统计数据letstatsPreferenceUtil.getInstance().get(this.STATISTICS_KEY,undefined)asPracticeStatistics;二、显式序列化的应用场景虽然 Preferences API 支持自动序列化但在某些场景下显式序列化仍然有必要2.1 复杂嵌套结构的深度复制// 学习计划克隆 - 使用手动深度复制而非 JSON 序列化privatecloneLearningPlan(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的部分更新中??空值合并运算符扮演了关键角色privatemergeLearningPlans(currentPlan:LearningPlan,plan:PartialLearningPlan):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.1as类型断言的局限性在 ArkTS 中as关键字只是编译时类型断言不会在运行时做类型校验// 编译时通过但运行时可能不是 NewWordItem[]letwordsPreferenceUtil.getInstance().get(this.NEW_WORD_KEY,[])asNewWordItem[];如果存储的数据格式被意外修改如版本升级改变了数据结构as断言不会抛出任何错误直到代码访问不存在的属性时才会出现运行时错误。3.2 运行时安全性检查更安全的做法是在反序列化后做校验或者提供默认值兜底// StatisticsManager 中的安全读取publicgetHistoryStatistics():PracticeStatistics{try{letstatsPreferenceUtil.getInstance().get(this.STATISTICS_KEY,undefined)asPracticeStatistics;if(!stats){returnthis.getEmptyStatistics();// 返回空统计而非 null}returnstats;}catch(e){Logger.error(StatisticsManager,获取历史统计数据失败:${JSON.stringify(e)});returnthis.getEmptyStatistics();}}3.3undefined作为默认值标识当需要区分未设置和空数组时使用undefined作为默认值// 学习计划 - undefined 表示还未设置过letplanPreferenceUtil.getInstance().get(this.LEARNING_PLAN_KEY,undefined)asLearningPlan;if(!plan){planthis.cloneLearningPlan(DEFAULT_LEARNING_PLAN);this.saveLearningPlan(plan);}// 生词本 - 空数组 [] 是有效值letwordsPreferenceUtil.getInstance().get(this.NEW_WORD_KEY,[])asNewWordItem[];四、日期对象的序列化陷阱日期对象的序列化是 JSON 处理中最常见的陷阱之一letdatenewDate(2025-01-15);letjsonJSON.stringify(date);console.log(json);// 2025-01-15T00:00:00.000Z —— 变成了字符串letparsedJSON.parse(json);console.log(parsedinstanceofDate);// false —— 不再是 Date 对象在项目中正确的做法是将日期存储为时间戳number而不是Date对象// 存储时间戳推荐addTime:Date.now()// number 类型安全可序列化// 读取后转换为 DateletaddDatenewDate(wordItem.addTime);// 连续天数比较中使用时间戳lettodaynewDate();today.setHours(0,0,0,0);lettodayTimestamptoday.getTime();// 存储的是 number最佳实践所有需要序列化的日期值都使用number时间戳而非Date对象。五、版本兼容与默认字段当应用版本升级、数据模型新增字段时旧数据中会缺少新字段。通过??和默认值可以优雅地处理版本兼容// 假设 LearningPlan 在新版本中新增了 reminderEnabled 字段// 旧数据中没有这个字段读取后为 undefined// 使用 ?? 可以提供默认值privatemergeLearningPlans(currentPlan:LearningPlan,plan:PartialLearningPlan):LearningPlan{return{reminderEnabled:plan.reminderEnabled??currentPlan.reminderEnabled,// undefined 时保留旧值// ...};}// 更好的方式是提供 DEFAULT 常量exportconstDEFAULT_LEARNING_PLAN:LearningPlan{reminderEnabled:false,// 新字段默认值reminderTime:08:00,// 新字段默认值// ...};六、最佳实践6.1 统一的序列化入口通过PreferenceUtil封装所有的序列化逻辑避免在各 Manager 中散落JSON.stringify调用。6.2 避免引用共享反序列化的对象与原始对象没有引用关系这是安全的。但在 Manager 内部操作时应避免直接修改反序列化的结果// ❌ 直接修改反序列化的引用letplanthis.getLearningPlan();plan.todayWordCountcount;// 修改了内存中的对象this.saveLearningPlan(plan);// 序列化后写入// ✅ 读取后立即写入是安全的读取-修改-写回模式6.3 日志序列化在 Logger 中记录复杂对象时记得使用JSON.stringifyLogger.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 中编写出既类型安全又健壮的数据持久化代码。

本月热点