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(): Promise<LeaderRecord[]> { 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): Promise<boolean> { 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; } }

核心问题

  1. @ohos.net.http怎么创建请求、设置方法、发请求?
  2. GET 和 POST 在参数传递、语义上有什么不同?
  3. 请求头、响应体怎么解析?
  4. 网络错误、超时怎么处理?

二、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 创建 HttpRequest

import { 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响应体(字符串或对象,看 header)
header响应头对象

2.4 销毁实例

aboutToDisappear() { this.httpRequest.destroy(); // 组件销毁时释放 HTTP 实例,避泄漏 }

关键经验aboutToDisappear 销毁 HttpRequest——不销毁可能内存泄漏,特别是频繁切页的应用。

三、GET/POST 方法差异

3.1 GET 拉数据

// GET:参数拼在 URL const url = 'https://api.catgame.com/leaderboard?limit=10&offset=0'; const res = await this.httpRequest.request(url, { method: http.RequestMethod.GET, header: { 'Content-Type': 'application/json' } });

GET 语义

  • 拉数据(读操作)。
  • 参数拼 URL(query 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 语义

  • 发数据(写操作/创建)。
  • 数据放 body(extraData)。
  • 非幂等(多次调可能创建多条记录)。
  • 不可缓存
  • 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/multipart)
Authorization鉴权(Bearer token / Basic auth)
Accept期望的响应格式
User-Agent客户端标识

4.2 Content-Type 对照

// JSON body header: { 'Content-Type': 'application/json' }, extraData: JSON.stringify({ score: 1500 }) // 表单 body(form-urlencoded) header: { 'Content-Type': 'application/x-www-form-urlencoded' }, extraData: 'score=1500&combo=5' // 文件上传(multipart/form-data) header: { 'Content-Type': 'multipart/form-data; boundary=----CatBoundary' }, extraData: '------CatBoundary\r\nContent-Disposition: form-data; name="file"; filename="cat.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(): Promise<LeaderRecord[]> { 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建立连接超时5000ms
readTimeout读取响应超时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.ets

import { 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): Promise<LeaderRecord[]> { 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): Promise<boolean> { 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.ets

export 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.ets

import { 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 endGame(HTTP 改造后) 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: 'score=1500&combo=5' // 不是 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,异步上报 }

八、调试技巧

  1. console.info打响应码和体:追服务器响应,验证接口。
  2. DevEco Network Inspector:查看请求/响应头和体,类似浏览器 Network 面板。
  3. 请求不发排查:检查 INTERNET 权限;检查 URL 是否对;检查是否在 aboutToAppear 调(组件未挂载)。
  4. 超时排查:检查 connectTimeout/readTimeout;检查网络是否通;检查服务器是否响应。

九、性能与最佳实践

  1. 联网必申请 INTERNET 权限——不申请请求被拒。
  2. 必设 connectTimeout/readTimeout——不设可能卡死。
  3. GET 拉数据,POST 发数据——语义清晰,幂等性正确。
  4. Content-Type 与 body 格式匹配——JSON 配 application/json + JSON.stringify。
  5. UI 层三态显示——loading/error/normal,错误时用本地缓存兜底。
  6. aboutToDisappear 销毁 HttpRequest——避泄漏。
  7. 异步上报不阻塞 UI——先切状态再异步上报,失败静默。
  8. 复用 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.etsentry/src/main/ets/pages/LeaderboardPage.etsentry/src/main/ets/pages/Index.ets
  • @ohos.net.http HTTP 模块官方指南
  • HarmonyOS 网络权限申请官方文档
  • HTTP RequestMethod 枚举官方文档
  • HarmonyOS 网络编程最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md