HarmonyOS应用开发实战:猫猫大作战-JSON stringify/parse、Protobuf 紧凑二进制、JSON vs Protobuf

发布时间:2026/7/28 0:28:07
HarmonyOS应用开发实战:猫猫大作战-JSON stringify/parse、Protobuf 紧凑二进制、JSON vs Protobuf 前言前面我们用 HTTP 拉排行榜、上报得分——服务器返回的是字符串本地 state 是对象两者间要序列化/反序列化转换。HarmonyOS 默认用JSON文本格式可读易调试但移动端省流量省电场景可选Protobuf二进制格式紧凑高效。本篇对比两者讲透序列化机制。本篇以「猫猫大作战」拉排行榜 JSON 解析、上报得分 JSON 创建、本地缓存 Protobuf 紧凑存储为锚点把JSON stringify/parse、Protobuf 紧凑二进制、JSON vs Protobuf 取舍、Schema 类型安全四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–62 篇。本篇是阶段四第三篇。一、场景拆解拉排行榜解析 上报创建回顾「猫猫大作战」HttpService第 61 篇// 拉服务器返回字符串要解析成对象 async fetchLeaderboard(): PromiseLeaderRecord[] { const res await this.httpRequest.request(url, { /* ... */ }); if (res.responseCode 200) { return JSON.parse(res.result) as LeaderRecord[]; // ← 反序列化 } } // 上报本地对象要转字符串发给服务器 async submitScore(data: ScoreSubmit): Promiseboolean { const res await this.httpRequest.request(url, { method: http.RequestMethod.POST, extraData: JSON.stringify(data), // ← 序列化 /* ... */ }); }核心问题JSON.stringify/JSON.parse怎么用嵌套对象/数组怎么处理Protobuf 比 JSON 省多少怎么用何时用 JSON 何时用 Protobuf怎么保证类型安全解析后不是any二、JSON stringify/parse2.1 stringify对象转字符串const data: ScoreSubmit { score: 1500, maxCombo: 5, mergeCount: 28, highestLevel: 3, duration: 180, date: 2026-07-24T10:30:00Z }; const json: string JSON.stringify(data); // {score:1500,maxCombo:5,mergeCount:28,highestLevel:3,duration:180,date:2026-07-24T10:30:00Z} // 美化带缩进调试用 const pretty: string JSON.stringify(data, null, 2); // { // score: 1500, // maxCombo: 5, // ... // }API 说明API作用JSON.stringify(obj)对象转 JSON 字符串紧凑JSON.stringify(obj, null, 2)带缩进美化调试用JSON.stringify(obj, replacer)自定义字段过滤/转换2.2 parse字符串转对象const json: string {score:1500,maxCombo:5,mergeCount:28}; const data JSON.parse(json) as ScoreSubmit; console.info(data.score); // 1500 console.info(data.maxCombo); // 5API 说明API作用JSON.parse(str)JSON 字符串转对象JSON.parse(str) as T强类型断言编译时校验2.3 嵌套对象/数组// 嵌套对象 const player: Player { id: p1, name: Fiona, stats: { highScore: 1500, totalScore: 12000 }, skins: [default, neon] }; const json: string JSON.stringify(player); // {id:p1,name:Fiona,stats:{highScore:1500,totalScore:12000},skins:[default,neon]} const parsed JSON.parse(json) as Player; console.info(parsed.stats.highScore); // 1500 console.info(parsed.skins[0]); // default关键经验JSON.stringify/parse 自动处理嵌套对象和数组——不用逐层手动转换。2.4 stringify 的坑不支持的字段const obj { func: () console.info(hi), // 函数 undef: undefined, // undefined sym: Symbol(s), // Symbol big: BigInt(123), // BigInt date: new Date() // Date 对象 }; JSON.stringify(obj); // {date:2026-07-24T10:30:00.000Z} // 函数、undefined、Symbol 被忽略BigInt 抛错Date 转 ISO 字符串实战经验Date 对象 stringify 转字符串parse 回的是字符串不是 Date——要手动new Date(parsed.date)复原。三、Protobuf 紧凑二进制3.1 为什么用 ProtobufJSON 是文本格式可读但体积大——字段名、引号、括号都占字节。移动端省流量省电场景用ProtobufProtocol Buffers二进制格式紧凑 30–80%但不可读。3.2 体积对比// JSON86 字节 {score:1500,maxCombo:5,mergeCount:28,highestLevel:3,duration:180} // Protobuf约 25 字节字段名用编号替代数值变长编码 // [08][96][17][10][05][18][1C][20][03][28][B4][01]数据JSONProtobuf节省排行榜 10 条~2KB~600B70%战绩 50 条~5KB~1.5KB70%上报得分86B25B70%关键经验Protobuf 紧凑 30–80%——字段名用编号field 1: score替score:数值变长编码1500 不用 4 字节而用 2 字节。3.3 Schema 定义Protobuf 要先写.protoSchema// entry/src/main/proto/score.proto syntax proto3; message ScoreSubmit { int32 score 1; int32 maxCombo 2; int32 mergeCount 3; int32 highestLevel 4; int32 duration 5; string date 6; } message LeaderRecord { int32 rank 1; string playerName 2; int32 score 3; string avatar 4; } message Leaderboard { repeated LeaderRecord records 1; }拆解片段含义syntax proto3Protobuf v3 语法message ScoreSubmit消息类型对应 TS 的 interfaceint32 score 1字段类型 名字 编号string date 6字符串字段repeated LeaderRecord records数组字段关键经验字段编号是 Protobuf 的标识——不像 JSON 用字段名用编号1/2/3紧凑省字节。3.4 HarmonyOS 端用 ProtobufHarmonyOS 提供kit.ProtobufKit或第三方 protobufjsimport { Protobuf } from kit.ProtobufKit; // 假设有生成的 ScoreSubmitProto 和 LeaderboardProto 类 // 对象 → 二进制 const scoreData: ScoreSubmit { score: 1500, maxCombo: 5, /* ... */ }; const buffer: Uint8Array Protobuf.encode(scoreData, ScoreSubmitProto); // 二进制 → 对象 const decoded: ScoreSubmit Protobuf.decode(buffer, ScoreSubmitProto) as ScoreSubmit; // HTTP 发二进制 const res await this.httpRequest.request(url, { method: http.RequestMethod.POST, header: { Content-Type: application/x-protobuf }, extraData: buffer });提示Protobuf 在 HarmonyOS 的具体 API 需查实际 SDK——kit.ProtobufKit或用第三方protobufjsnpm 包。Schema 编译为 TS 类用protoc工具。四、JSON vs Protobuf 取舍4.1 对比表维度JSONProtobuf格式文本二进制体积大紧凑 30–80%可读✅ 人类可读❌ 不可读调试✅ console 直接看❌ 要解码Schema❌ 无靠文档✅ .proto 强类型工具✅ 成熟通用中要 protoc 编译字段冗余必传字段名字段编号省兼容改字段名即破坏加字段不破坏老客户端适合调试、人类可读、小数据移动端、大数据、省流量4.2 取舍决策数据规模 ├ 小 1KB且要调试 → JSON └ 大≥ 10KB省流量 → Protobuf 人类可读需求 ├ 要调试/日志 → JSON └ 不要 → Protobuf 可 团队熟悉度 ├ 熟 JSON → JSON └ 愿学 Protobuf 有 Schema 工具 → Protobuf 兼容性需求 ├ 字段常变、严格兼容 → Protobuf加字段不破坏 └ 字段稳定 → JSON4.3 「猫猫大作战」推荐场景推荐原因拉排行榜10 条2KBJSON小数据可读易调试上报得分86BJSON极小数据不值得 Protobuf 开销战绩列表50 条5KBProtobuf省流量 3.5KB本地缓存存盘Protobuf省存储空间调试日志JSON可读易排查实战经验「猫猫大作战」主体用 JSON——数据规模小、调试友好只有战绩列表和本地缓存用 Protobuf 省流量省空间。五、Schema 类型安全5.1 JSON.parse 的 any 陷阱// ❌ 错误JSON.parse 返回 any拼错字段名不报错 const data JSON.parse(json); console.info(data.scor); // undefined拼错 score运行时才发现 // ✅ 正确as T 强类型断言 const data JSON.parse(json) as ScoreSubmit; console.info(data.score); // 编译时校验 score 存在5.2 接口定义对应 JSON// 来源entry/src/main/ets/components/GameTypes.ets export interface ScoreSubmit { score: number; maxCombo: number; mergeCount: number; highestLevel: number; duration: number; date: string; } // JSON: {score:1500,maxCombo:5,...} // 接口字段名与 JSON 键名一一对应 const data JSON.parse(json) as ScoreSubmit;关键经验接口字段名与 JSON 键名一一对应——as T才能正确断言拼错运行时 undefined。5.3 运行时校验边界// 服务器返回不可信关键边界要运行时校验 function parseScore(json: string): ScoreSubmit { const data JSON.parse(json) as ScoreSubmit; if (typeof data.score ! number || data.score 0) { throw new Error(score 字段非法); } if (typeof data.date ! string || !Date.parse(data.date)) { throw new Error(date 字段非法); } return data; }实战经验系统边界网络/文件输入做运行时校验——as T是编译时断言服务器返回的字段可能缺失或类型错。5.4 Protobuf Schema 强类型message ScoreSubmit { int32 score 1; // 必是 int32不是 string string date 6; }关键经验Protobuf Schema 编译时强类型——不像 JSON 的as T只是断言Protobuf 编译时就校验字段类型。六、完整实战序列化服务6.1 创建 SerializationService新建entry/src/main/ets/services/SerializationService.etsimport { LeaderRecord, ScoreSubmit, GameRecord, Player } from ../components/GameTypes; export class SerializationService { // JSON 序列化网络通信用 // 对象 → JSON 字符串紧凑发服务器 static toJson(obj: Object): string { return JSON.stringify(obj); } // JSON 字符串 → 对象拉服务器后解析 static fromJsonT(json: string, type: new () T): T { try { const data JSON.parse(json) as T; SerializationService.validate(data, type); return data; } catch (error) { throw new Error(JSON 解析失败: ${(error as Error).message}); } } // 数组解析 static fromJsonArrayT(json: string, type: new () T): T[] { try { const data JSON.parse(json) as T[]; data.forEach(item SerializationService.validate(item, type)); return data; } catch (error) { throw new Error(JSON 数组解析失败: ${(error as Error).message}); } } // 运行时校验关键字段 private static validateT(data: T, type: new () T): void { if (!data) { throw new Error(数据为空); } // 简化实际可遍历 type 的必需字段校验 // 用 reflect 或手写校验逻辑 } // 美化 JSON调试/日志用 static toPrettyJson(obj: Object): string { return JSON.stringify(obj, null, 2); } // Date 复原JSON.stringify 把 Date 转 ISO 字符串 static reviveDateT(data: T): T { // 简化遍历字段如果是 ISO 字符串转 Date // 实际可遍历 data 的 date 字段 return data; } // 本地缓存用紧凑 JSON无缩进 static toCompactJson(obj: Object): string { return JSON.stringify(obj); // 默认就是紧凑无缩进 } // Protobuf如果项目用了 // static encodeProtobufT(obj: T, proto: any): Uint8Array { // return Protobuf.encode(obj, proto); // } // // static decodeProtobufT(buffer: Uint8Array, proto: any): T { // return Protobuf.decode(buffer, proto) as T; // } }6.2 改造 HttpService 用 SerializationService// 来源entry/src/main/ets/services/HttpService.ets序列化改造后 import { http } from kit.NetworkKit; import { SerializationService } from ./SerializationService; import { LeaderRecord, ScoreSubmit } from ../components/GameTypes; export class HttpService { private httpRequest: http.HttpRequest http.createHttp(); private readonly baseUrl: string https://api.catgame.com; async fetchLeaderboard(): PromiseLeaderRecord[] { const res await this.httpRequest.request(${this.baseUrl}/leaderboard?limit10, { method: http.RequestMethod.GET, header: { Content-Type: application/json }, connectTimeout: 5000, readTimeout: 5000 }); if (res.responseCode 200) { // 用 SerializationService 解析带校验 return SerializationService.fromJsonArrayLeaderRecord(res.result, LeaderRecord); } throw new Error(HTTP ${res.responseCode}); } async submitScore(data: ScoreSubmit): Promiseboolean { // 用 SerializationService 序列化 const json SerializationService.toJson(data); const res await this.httpRequest.request(${this.baseUrl}/score, { method: http.RequestMethod.POST, header: { Content-Type: application/json }, extraData: json, connectTimeout: 5000, readTimeout: 10000 }); return res.responseCode 200 || res.responseCode 201; } destroy() { this.httpRequest.destroy(); } }6.3 调试日志用美化 JSON// 调试时打美化 JSON可读 const data: ScoreSubmit { score: 1500, maxCombo: 5, /* ... */ }; console.info(上报数据:\n SerializationService.toPrettyJson(data)); // 输出 // 上报数据: // { // score: 1500, // maxCombo: 5, // ... // }七、踩坑提示7.1 JSON.parse 返回 any 不 as T// ❌ 错误返回 any拼错字段运行时 undefined const data JSON.parse(json); console.info(data.scor); // undefined // ✅ 正确as T 强类型断言 const data JSON.parse(json) as ScoreSubmit; console.info(data.score);7.2 Date 对象不复原// ❌ 错误parse 后 date 是字符串不是 Date const data JSON.parse(json) as { date: Date }; console.info(data.date.getFullYear()); // 报错date 是字符串 // ✅ 正确手动复原 const data JSON.parse(json) as { date: string }; const dateObj new Date(data.date); console.info(dateObj.getFullYear());7.3 stringify 忘处理 undefined/函数// ⚠️ stringify 自动忽略 undefined/函数/Symbol const obj { score: 1500, note: undefined }; JSON.stringify(obj); // {score:1500}note 没了 // 如果要保留 undefined用 replacer JSON.stringify(obj, (key, value) value undefined ? null : value); // {score:1500,note:null}7.4 BigInt 报错// ❌ 错误BigInt 抛错 const obj { big: BigInt(123) }; JSON.stringify(obj); // TypeError: Do not know how to serialize a BigInt // ✅ 正确转字符串或数字 const obj { big: 123 }; // 字符串 const obj { big: 123 }; // 数字够小7.5 Protobuf 忘编译 Schema// ❌ 错误没 protoc 编译 .proto没 ScoreSubmitProto 类 const buffer Protobuf.encode(data, ScoreSubmitProto); // 报错未定义 // ✅ 正确先 protoc 编译 // protoc --ts_out./proto score.proto // 生成 ScoreSubmitProto 类后再用7.6 Content-Type 与格式不匹配// ❌ 错误说是 Protobuf 但 Content-Type 是 JSON extraData: protobufBuffer, header: { Content-Type: application/json } // 错 // ✅ 正确Protobuf 配 x-protobuf header: { Content-Type: application/x-protobuf }八、调试技巧console.info打美化 JSON用JSON.stringify(obj, null, 2)看清字段。解析失败排查检查 JSON 字符串是否完整检查字段名是否与接口对应检查类型是否匹配。Date 复原排查parse 后 logtypeof data.date如果是 string 说明没复原。Protobuf 解码排查用Protobuf.toString(buffer)或调试工具解码二进制看内容。九、性能与最佳实践网络小数据用 JSON——可读易调试开销可接受。大数据/本地缓存用 Protobuf——紧凑 30–80% 省流量省空间。JSON.parse 必 as T 强类型——避免 any 拼错字段运行时 undefined。系统边界做运行时校验——服务器返回不可信关键字段校验类型。Date 对象 parse 后手动复原——JSON.stringify 转 ISO 字符串parse 回是字符串。Protobuf 先 protoc 编译 Schema——生成 TS 类后才能 encode/decode。Content-Type 与格式匹配——JSON 配 application/jsonProtobuf 配 x-protobuf。调试日志用美化 JSON——JSON.stringify(obj, null, 2)可读。十、阶段四进度61–65本篇是阶段四「网络与数据」第 3 篇篇主题核心要点61HTTP 请求http 模块、GET/POST、错误处理62REST/GraphQL接口设计取舍63本篇数据序列化JSON stringify/parse、Protobuf 紧凑、取舍64SQLite 持久化关系型数据存储65Preference 键值轻量配置存储总结本篇我们从数据序列化切入掌握了JSON stringify/parse自动处理嵌套Date 要复原、Protobuf 紧凑二进制省 30–80%Schema 编译、取舍小数据 JSON大数据 Protobuf、**Schema 类型安全as T 边界校验**四大要点并给出了 SerializationService 完整封装代码。核心要点JSON 可读易调试Protobuf 紧凑省流量parse 必 as TDate 手动复原边界运行时校验Content-Type 配格式。下一篇我们将拆解 SQLite——关系型数据持久化。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/services/SerializationService.ets、entry/src/main/ets/services/HttpService.ets、entry/src/main/ets/components/GameTypes.etsArkTS JSON API 官方文档Protocol Buffers 官方文档HarmonyOS 数据序列化最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md