HarmonyOS应用开发实战:猫猫大作战-http 模块创建与请求、GET/POST 方法差异、请求头与响应体解析、错误处理与超时 前言前面 60 篇我们都在本地数据驱动 UI——State、Local、引擎 GameEngine。但实战应用要联网拉取战绩排行榜、上报得分、下载猫咪皮肤、检查更新。HarmonyOS 提供ohos.net.http官方 HTTP 模块做远程接口调用类似 Web 的fetch/axios。本篇以「猫猫大作战」拉取全球排行榜、上报本局得分为场景把http 模块创建与请求、GET/POST 方法差异、请求头与响应体解析、错误处理与超时四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–60 篇。本篇是阶段四第一篇进入「网络与数据」阶段。一、场景拆解拉排行榜 上报得分「猫猫大作战」当前只显示本地highScore第 31 篇——玩家想看全球排行榜开游戏时从服务器拉 Top 10 排行。结束游戏时上报本局得分到服务器。// 预演HttpService 拉排行榜 上报得分 import { http } from kit.NetworkKit; export class HttpService { private httpRequest: http.HttpRequest http.createHttp(); // GET 拉排行榜 async fetchLeaderboard(): PromiseLeaderRecord[] { const res await this.httpRequest.request(https://api.catgame.com/leaderboard, { method: http.RequestMethod.GET, header: { Content-Type: application/json }, connectTimeout: 5000, readTimeout: 5000 }); if (res.responseCode 200) { return JSON.parse(res.result) as LeaderRecord[]; } throw new Error(拉排行榜失败: ${res.responseCode}); } // POST 上报得分 async submitScore(score: number, combo: number): Promiseboolean { const res await this.httpRequest.request(https://api.catgame.com/score, { method: http.RequestMethod.POST, header: { Content-Type: application/json }, extraData: JSON.stringify({ score, maxCombo: combo, time: Date.now() }), connectTimeout: 5000, readTimeout: 5000 }); return res.responseCode 200; } }核心问题ohos.net.http怎么创建请求、设置方法、发请求GET 和 POST 在参数传递、语义上有什么不同请求头、响应体怎么解析网络错误、超时怎么处理二、http 模块创建与请求2.1 申请网络权限修改entry/src/main/module.json5或config.json{ module: { requestPermissions: [ { name: ohos.permission.INTERNET, reason: $string:reason_internet, usedScene: { abilities: [EntryAbility], when: always } } ] } }关键经验联网必先申请 INTERNET 权限——不申请运行时请求被拒http.request抛权限错。2.2 创建 HttpRequestimport { http } from kit.NetworkKit; export class HttpService { private httpRequest: http.HttpRequest http.createHttp(); // 每次请求复用同一实例也可每次新建 }拆解API作用http.createHttp()创建 HttpRequest 实例后续请求都调它http.HttpRequest类型实例化后调.request()发请求2.3 发请求const res: http.HttpResponse await this.httpRequest.request(url, options);返回 HttpResponse字段含义responseCodeHTTP 响应码200/404/500 等result响应体字符串或对象看 headerheader响应头对象2.4 销毁实例aboutToDisappear() { this.httpRequest.destroy(); // 组件销毁时释放 HTTP 实例避泄漏 }关键经验aboutToDisappear 销毁 HttpRequest——不销毁可能内存泄漏特别是频繁切页的应用。三、GET/POST 方法差异3.1 GET 拉数据// GET参数拼在 URL const url https://api.catgame.com/leaderboard?limit10offset0; const res await this.httpRequest.request(url, { method: http.RequestMethod.GET, header: { Content-Type: application/json } });GET 语义拉数据读操作。参数拼 URLquery string。幂等多次调同样效果。可缓存浏览器/CDN 可缓存 GET 响应。URL 长度限制通常 2KB。3.2 POST 发数据// POST数据放 body const res await this.httpRequest.request(https://api.catgame.com/score, { method: http.RequestMethod.POST, header: { Content-Type: application/json }, extraData: JSON.stringify({ score: 1500, maxCombo: 5, time: Date.now() }) });POST 语义发数据写操作/创建。数据放 bodyextraData。非幂等多次调可能创建多条记录。不可缓存。body 无长度限制受服务器配置。3.3 对比表维度GETPOST语义读写/创建参数位置URL querybody幂等✅❌可缓存✅❌长度 2KB无限安全参数暴露 URLbody 隐藏关键经验「拉数据」用 GET「发数据」用 POST——拉排行榜 GET上报得分 POST。3.4 其他方法enum RequestMethod { OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT }方法语义本项目用GET读✅ 拉排行榜POST创建✅ 上报得分PUT更新整体可选改用户信息DELETE删除可选删战绩PATCH更新局部可选改昵称四、请求头与响应体解析4.1 请求头设置const res await this.httpRequest.request(url, { method: http.RequestMethod.POST, header: { Content-Type: application/json, // body 是 JSON Authorization: Bearer this.token, // 鉴权令牌 Accept: application/json, // 接受 JSON 响应 User-Agent: CatGame/1.0 HarmonyOS // 客户端标识 }, extraData: JSON.stringify({ /* ... */ }) });常用请求头头作用Content-Typebody 格式application/json/form-urlencoded/multipartAuthorization鉴权Bearer token / Basic authAccept期望的响应格式User-Agent客户端标识4.2 Content-Type 对照// JSON body header: { Content-Type: application/json }, extraData: JSON.stringify({ score: 1500 }) // 表单 bodyform-urlencoded header: { Content-Type: application/x-www-form-urlencoded }, extraData: score1500combo5 // 文件上传multipart/form-data header: { Content-Type: multipart/form-data; boundary----CatBoundary }, extraData: ------CatBoundary\r\nContent-Disposition: form-data; namefile; filenamecat.png\r\n...关键经验Content-Type必须与 body 格式匹配——JSON 配 application/json表单配 form-urlencoded文件配 multipart。4.3 响应体解析const res await this.httpRequest.request(url, { /* ... */ }); // 响应码 if (res.responseCode 200) { // result 通常是字符串JSON 字符串 const data JSON.parse(res.result) as LeaderRecord[]; // 如果服务器返回 Content-Type: application/json某些版本 result 直接是对象 // const data res.result as LeaderRecord[]; }两种 result 类型服务器 Content-Typeresult 类型解析application/json字符串JSON 字符串或对象版本差异JSON.parse或直接用text/html字符串直接用application/octet-streamArrayBuffer二进制处理实战经验保险起见JSON.parse try-catch——兼容 result 是字符串或对象的情况。4.4 响应头读取const res await this.httpRequest.request(url, { /* ... */ }); // 读响应头 const contentType res.header[Content-Type]; const serverVersion res.header[X-Version]; const rateLimit res.header[X-RateLimit-Remaining];五、错误处理与超时5.1 try-catch 错误处理async fetchLeaderboard(): PromiseLeaderRecord[] { try { const res await this.httpRequest.request(url, { /* ... */ }); if (res.responseCode 200) { return JSON.parse(res.result) as LeaderRecord[]; } else if (res.responseCode 401) { throw new Error(未授权请登录); } else if (res.responseCode 404) { throw new Error(接口不存在); } else if (res.responseCode 500) { throw new Error(服务器错误); } else { throw new Error(HTTP ${res.responseCode}); } } catch (error) { console.error(拉排行榜失败: ${error.message}); throw error; // 向上抛UI 层处理 } }5.2 超时设置const res await this.httpRequest.request(url, { method: http.RequestMethod.GET, connectTimeout: 5000, // 连接超时 5s readTimeout: 5000 // 读取超时 5s });超时含义推荐connectTimeout建立连接超时5000msreadTimeout读取响应超时5000–10000ms关键经验必设超时——不设可能卡死网络不通时无限等。5.3 UI 层错误处理State leaderboard: LeaderRecord[] []; State loading: boolean false; State errorMsg: string ; async loadLeaderboard() { this.loading true; this.errorMsg ; try { this.leaderboard await this.httpService.fetchLeaderboard(); } catch (error) { this.errorMsg error.message; // 用本地缓存兜底第 48 篇 LazyForEach 思想 this.leaderboard this.loadLocalLeaderboard(); } finally { this.loading false; } } Builder LeaderboardView() { Column() { if (this.loading) { Text(加载中...).fontSize(16).fontColor(#95A5A6) } else if (this.errorMsg ! ) { Column() { Text(⚠️ 加载失败).fontSize(16).fontColor(#E74C3C) Text(this.errorMsg).fontSize(12).fontColor(#95A5A6).margin({ top: 4 }) Button(重试).onClick(() { this.loadLeaderboard(); }) .margin({ top: 12 }).backgroundColor(#3498DB) } } else { // 正常显示排行榜 ForEach(this.leaderboard, (record: LeaderRecord) { /* ... */ }, (record: LeaderRecord) record.rank) } } }关键经验UI 层三态显示——loading加载中、error失败重试、normal正常显示。六、完整实战HttpService 排行榜页6.1 创建 HttpService新建entry/src/main/ets/services/HttpService.etsimport { http } from kit.NetworkKit; import { LeaderRecord, ScoreSubmit } from ../components/GameTypes; export class HttpService { private httpRequest: http.HttpRequest http.createHttp(); private readonly baseUrl: string https://api.catgame.com; // GET 拉排行榜 async fetchLeaderboard(limit: number 10, offset: number 0): PromiseLeaderRecord[] { const url ${this.baseUrl}/leaderboard?limit${limit}offset${offset}; try { const res await this.httpRequest.request(url, { method: http.RequestMethod.GET, header: { Content-Type: application/json, Accept: application/json, Authorization: Bearer (this.token || ) }, connectTimeout: 5000, readTimeout: 5000 }); if (res.responseCode 200) { return JSON.parse(res.result) as LeaderRecord[]; } else if (res.responseCode 401) { throw new Error(未授权请登录); } else { throw new Error(HTTP ${res.responseCode}); } } catch (error) { console.error(fetchLeaderboard 失败: ${(error as Error).message}); throw error; } } // POST 上报得分 async submitScore(data: ScoreSubmit): Promiseboolean { const url ${this.baseUrl}/score; try { const res await this.httpRequest.request(url, { method: http.RequestMethod.POST, header: { Content-Type: application/json, Accept: application/json, Authorization: Bearer (this.token || ) }, extraData: JSON.stringify(data), connectTimeout: 5000, readTimeout: 10000 }); if (res.responseCode 200 || res.responseCode 201) { return true; } else if (res.responseCode 401) { throw new Error(未授权请登录); } else { throw new Error(HTTP ${res.responseCode}); } } catch (error) { console.error(submitScore 失败: ${(error as Error).message}); throw error; } } private token: string ; setToken(token: string) { this.token token; } // 销毁 destroy() { this.httpRequest.destroy(); } }6.2 补充类型修改entry/src/main/ets/components/GameTypes.etsexport interface LeaderRecord { rank: number; playerName: string; score: number; maxCombo: number; date: string; } export interface ScoreSubmit { score: number; maxCombo: number; mergeCount: number; highestLevel: number; duration: number; // 用时秒 date: string; }6.3 创建 LeaderboardPage新建entry/src/main/ets/pages/LeaderboardPage.etsimport { LeaderRecord } from ../components/GameTypes; import { HttpService } from ../services/HttpService; Component export struct LeaderboardPage { State leaderboard: LeaderRecord[] []; State loading: boolean false; State errorMsg: string ; private httpService: HttpService new HttpService(); aboutToAppear() { this.loadLeaderboard(); } aboutToDisappear() { this.httpService.destroy(); // 销毁 HTTP 实例避泄漏 } async loadLeaderboard() { this.loading true; this.errorMsg ; try { this.leaderboard await this.httpService.fetchLeaderboard(10, 0); } catch (error) { this.errorMsg (error as Error).message; // 用本地缓存兜底 this.leaderboard this.loadLocalLeaderboard(); } finally { this.loading false; } } loadLocalLeaderboard(): LeaderRecord[] { // 简化返回空数组或本地缓存第 65 篇会专讲 Preference return []; } build() { Column() { // 标题栏 Row() { Text( 全球排行榜).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#2C3E50) } .width(100%).height(56).padding({ left: 16 }) .justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Center) // 内容区三态显示 if (this.loading) { Column() { LoadingProgress().width(48).height(48).color(#3498DB) Text(加载中...).fontSize(16).fontColor(#95A5A6).margin({ top: 12 }) } .width(100%).layoutWeight(1) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) } else if (this.errorMsg ! this.leaderboard.length 0) { Column() { Text(⚠️).fontSize(48).margin({ bottom: 12 }) Text(加载失败).fontSize(18).fontColor(#E74C3C).margin({ bottom: 4 }) Text(this.errorMsg).fontSize(12).fontColor(#95A5A6).margin({ bottom: 16 }) Button(重试) .width(120).height(40) .fontSize(16).fontColor(#FFFFFF).backgroundColor(#3498DB) .borderRadius(20) .onClick(() { this.loadLeaderboard(); }) } .width(100%).layoutWeight(1) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) } else if (this.leaderboard.length 0) { Column() { Text().fontSize(48).margin({ bottom: 12 }) Text(暂无排行数据).fontSize(16).fontColor(#95A5A6) } .width(100%).layoutWeight(1) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) } else { List() { ForEach(this.leaderboard, (record: LeaderRecord) { ListItem() { Row() { // 排名 Text(#${record.rank}).fontSize(18).fontWeight(FontWeight.Bold) .fontColor(record.rank 3 ? #E67E22 : #7F8C8D) .width(60) // 玩家名 Text(record.playerName).fontSize(16).fontColor(#2C3E50) .layoutWeight(1) // 得分 Text(record.score.toString()).fontSize(18).fontWeight(FontWeight.Bold) .fontColor(#2ECC71) } .width(100%).height(56) .padding({ left: 16, right: 16 }) .border({ width: { bottom: 1 }, color: #ECF0F1 }) } }, (record: LeaderRecord) rank_${record.rank}) } .width(100%).layoutWeight(1) .divider({ strokeWidth: 1, color: #ECF0F1, startMargin: 16, endMargin: 16 }) } // 错误时底部提示有本地缓存兜底也显示 if (this.errorMsg ! this.leaderboard.length 0) { Text(⚠️ ${this.errorMsg}显示本地缓存) .fontSize(12).fontColor(#95A5A6) .padding({ left: 16, right: 16, bottom: 8 }) } } .width(100%).height(100%) .backgroundColor(#FFFFFF) } }6.4 Index 结束游戏时上报得分// 来源entry/src/main/ets/pages/Index.ets endGameHTTP 改造后 import { HttpService } from ../services/HttpService; Entry Component struct Index { /* ... 其他 state 和成员 */ private httpService: HttpService new HttpService(); endGame() { this.gameState GameState.GAME_OVER; this.maxCombo this.gameEngine.getMaxCombo(); this.mergeCount this.gameEngine.getMergeCount(); this.highestLevel this.gameEngine.getHighestLevel(); if (this.score this.highScore) { this.highScore this.score; } this.clearTimers(); // 异步上报得分不阻塞 UI失败静默 this.submitScoreToServer(); } async submitScoreToServer() { try { const data: ScoreSubmit { score: this.score, maxCombo: this.maxCombo, mergeCount: this.mergeCount, highestLevel: this.highestLevel, duration: this.gameTime, date: new Date().toISOString() }; const success await this.httpService.submitScore(data); if (success) { console.info(得分上报成功); } } catch (error) { // 静默失败不阻塞玩家 console.error(得分上报失败: ${(error as Error).message}); } } aboutToDisappear() { this.clearTimers(); this.httpService.destroy(); // 销毁 HTTP 实例 } /* ... 其他方法 */ }七、踩坑提示7.1 忘申请 INTERNET 权限// ❌ 错误没申请权限请求被拒 // module.json5 里没 requestPermissions // ✅ 正确申请 INTERNET { module: { requestPermissions: [ { name: ohos.permission.INTERNET, reason: $string:reason_internet } ] } }7.2 忘设超时卡死// ❌ 错误没超时网络不通时无限等 const res await this.httpRequest.request(url, { method: http.RequestMethod.GET // 忠了 connectTimeout / readTimeout }); // ✅ 正确必设超时 const res await this.httpRequest.request(url, { method: http.RequestMethod.GET, connectTimeout: 5000, readTimeout: 5000 });7.3 Content-Type 与 body 格式不匹配// ❌ 错误说是 JSON 但 body 是字符串拼接 header: { Content-Type: application/json }, extraData: score1500combo5 // 不是 JSON // ✅ 正确JSON 配 JSON.stringify header: { Content-Type: application/json }, extraData: JSON.stringify({ score: 1500, combo: 5 })7.4 忘销毁 HttpRequest 泄漏// ❌ 错误没 destroy组件销毁后实例泄漏 aboutToDisappear() { this.clearTimers(); // 忠了 this.httpService.destroy(); } // ✅ 正确销毁释放 aboutToDisappear() { this.clearTimers(); this.httpService.destroy(); }7.5 UI 层忘错误处理// ❌ 错误没 try-catch网络失败时崩溃 async loadLeaderboard() { this.leaderboard await this.httpService.fetchLeaderboard(); // 抛错未捕 } // ✅ 正确try-catch 三态显示 async loadLeaderboard() { this.loading true; try { this.leaderboard await this.httpService.fetchLeaderboard(); } catch (error) { this.errorMsg (error as Error).message; this.leaderboard this.loadLocalLeaderboard(); // 缓存兜底 } finally { this.loading false; } }7.6 上报得分阻塞 UI// ❌ 错误endGame 里 await 上报玩家点结束要等上报完才看到结束弹窗 async endGame() { /* ... */ await this.submitScoreToServer(); // 阻塞 this.gameState GameState.GAME_OVER; // 上报完才切玩家等 } // ✅ 正确异步上报不阻塞失败静默 endGame() { this.gameState GameState.GAME_OVER; // 先切结束态 /* ... */ this.submitScoreToServer(); // 不 await异步上报 }八、调试技巧console.info打响应码和体追服务器响应验证接口。DevEco Network Inspector查看请求/响应头和体类似浏览器 Network 面板。请求不发排查检查 INTERNET 权限检查 URL 是否对检查是否在 aboutToAppear 调组件未挂载。超时排查检查 connectTimeout/readTimeout检查网络是否通检查服务器是否响应。九、性能与最佳实践联网必申请 INTERNET 权限——不申请请求被拒。必设 connectTimeout/readTimeout——不设可能卡死。GET 拉数据POST 发数据——语义清晰幂等性正确。Content-Type 与 body 格式匹配——JSON 配 application/json JSON.stringify。UI 层三态显示——loading/error/normal错误时用本地缓存兜底。aboutToDisappear 销毁 HttpRequest——避泄漏。异步上报不阻塞 UI——先切状态再异步上报失败静默。复用 HttpRequest 实例——不要每次请求新建开销大。十、阶段四进度开篇61–65本篇是阶段四「网络与数据」第 1 篇开篇覆盖篇主题核心要点61本篇HTTP 请求http 模块、GET/POST、错误处理62REST/GraphQL接口设计取舍63数据序列化JSON/proto 创建解析64SQLite 持久化关系型数据存储65Preference 键值轻量配置存储接下来阶段四还会覆盖文件 IO、数据绑定、列表分页、下拉刷新、上拉加载、离线缓存等。总结本篇我们从 HTTP 请求切入掌握了http 模块创建与请求createHttp request、GET/POST 方法差异读 vs 写幂等 vs 非幂等、请求头Content-Type 必配 body与响应体解析JSON.parse、**错误处理与超时try-catch 三态显示**四大要点并给出了 HttpService 排行榜页 上报得分的完整代码。核心要点联网先申请权限必设超时GET 拉数据 POST 发数据UI 三态显示缓存兜底aboutToDisappear 销毁异步上报不阻塞。下一篇我们将拆解 REST/GraphQL——接口设计取舍。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/services/HttpService.ets、entry/src/main/ets/pages/LeaderboardPage.ets、entry/src/main/ets/pages/Index.etsohos.net.http HTTP 模块官方指南HarmonyOS 网络权限申请官方文档HTTP RequestMethod 枚举官方文档HarmonyOS 网络编程最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md