HarmonyOS 实战 | 网络请求怎么做——用一个例子学会 fetch 一个接口

一、我们想解决什么问题
1.1 场景还原:用户打开 App,界面要显示服务器上的数据
想象一下这个场景:你在写一个社交类的小应用,用户打开 App 之后,界面上需要展示"当前登录用户的头像"和"用户名"。这数据不在手机本地,而是在服务器上。那怎么办?
你得去"问"服务器:“嘿,能把这个用户的信息给我吗?” 服务器收到问题,查了查数据库,把结果回给你。你收到之后再把头像和名字填到界面上去。
这个"问→等→答→更新"的过程,就是网络请求。
听起来不复杂对吧?但实际动手写代码的时候,问题就来了:请求发出去之后,程序不能卡在那儿等结果——否则用户看到的就是一个完全卡住不动、像死机一样的界面。那怎么做到"不等"呢?这里就涉及到一个核心概念:异步。
1.2 异步是什么?打个比方就懂了
同步的意思是:你让朋友帮你买杯咖啡,你就站在奶茶店门口干等,等他买完回来你再继续走路。异步的意思是:你说"帮我买杯咖啡",然后自己先走了,等朋友买好了给你发消息,你再回来拿。
在代码里,"发消息"就是发起网络请求,“等朋友买咖啡"就是等待响应。如果你用同步写法,程序会真的"站在原地干等”——界面卡死,用户点不了任何东西,体验极差。如果你用异步写法,程序先去做别的事,等数据回来了再更新界面,用户看到的是一个流畅的加载过程,而不是卡死。
在 HarmonyOS 的 ArkUI 里,我们用 @kit.NetworkKit 提供的 HTTP 工具来完成这件事。它的能力和浏览器里的 fetch 类似,但 API 设计上有点不一样,需要专门学一下。
1.3 这篇文章我们要做到什么
这篇文章我们就用 AtomGit 的场景来练手。AtomGit 是一个代码托管平台,类似于 GitHub。我们做一个简单的页面,用 HarmonyOS 应用从 GitHub 的公开 API 拉取某个用户的信息,然后展示到界面上。
具体来说,我们要展示:
- 用户的头像
- 用户名(login)
- 公开仓库数量
- 粉丝数量
- 关注数量
- 个人简介
整个过程拆成这几步:定模型 → 写请求 → 拿数据 → 展示界面。我们就按这个顺序一路讲下去。
二、数据模型设计
2.1 为什么先设计模型,而不是先写请求?
动手之前,先把"我们想要什么样的数据"定义清楚。就像盖房子先画图纸,数据模型就是你的图纸。很多新手喜欢直接写请求代码,写完才发现数据结构乱七八糟,字段名对不上,取数据的时候到处乱找。这就是没有先设计模型的代价。
而且,HarmonyOS 用的是 TypeScript 扩展(ArkTS),它有类型检查的能力。如果你先定义了接口(interface),代码里用错了字段名、传错了类型,IDE 直接报错,根本编译不过。如果你没定义接口,用错了字段名 IDE 也发现不了,只有运行时才发现数据是 undefined,然后页面展示一片空白,排查起来非常痛苦。
2.2 GitHub API 返回的原始数据长什么样
在我们定义自己的模型之前,先看看 GitHub API 实际返回的是什么。GitHub 公开 API 的地址是 https://api.github.com/users/{username},返回的数据大概长这样:
{
"login": "atomgit",
"avatar_url": "https://avatars.githubusercontent.com/u/xxxxx?v=4",
"public_repos": 42,
"followers": 128,
"following": 35,
"bio": "开源让世界更美好 🌟"
}
注意到字段名的风格了吗?GitHub API 用的是下划线命名(snake_case),比如 avatar_url、public_repos。这种命名风格在很多后端接口里很常见。
2.3 定义我们自己的数据模型
我们自己用的模型,字段名应该符合 ArkUI 的习惯(驼峰命名 camelCase)。所以我们定义一个 UserProfile 接口,然后写一个转换函数,把 API 返回的下划线字段映射到我们的驼峰字段上:
// UserProfile.ets
export interface UserProfile {
login: string // 用户名
avatarUrl: string // 头像地址(映射自 avatar_url)
publicRepos: number // 公开仓库数量(映射自 public_repos)
followers: number // 粉丝数量
following: number // 关注数量
bio: string // 个人简介
}
为什么要单独定义这个 interface,而不直接用 API 返回的结果?
打个比方:你去便利店买东西,收银台给你找了零钱,你随手塞口袋里不管了。但如果你提前想好"我需要知道找回的硬币是几枚、纸币是几张",你就会专门留意。同样,定义了 UserProfile interface 之后,代码里每个用到用户数据的地方都清楚知道有哪些字段可用,IDE 也能帮你检查有没有写错。
2.4 字段映射转换函数
光定义 interface 还不够,还需要一个转换函数,把 API 的原始数据转成我们自己的结构:
// GitHubUser 是 API 返回的原始数据结构
interface GitHubUser {
login: string
avatar_url: string
public_repos: number
followers: number
following: number
bio: string | null
}
export function convertToUserProfile(raw: GitHubUser): UserProfile {
return {
login: raw.login,
avatarUrl: raw.avatar_url,
publicRepos: raw.public_repos,
followers: raw.followers,
following: raw.following,
bio: raw.bio ?? '这个人很懒,什么都没写'
}
}
raw.bio ?? '这个人很懒,什么都没写' 这行代码的意思是:如果 bio 是 null 或者 undefined,就使用默认值。为什么要这样做?因为 API 返回的 bio 字段可能是 null,如果你直接 raw.bio 赋值给字符串类型,TypeScript 会报错,用 ?? 操作符给一个默认值就解决了。
三、核心设计决策
3.1 方案对比:从零开始写网络请求的两条路
从零开始做网络请求,你面前有两条路:
方案A:直接用 @kit.NetworkKit 工具包
每次需要请求数据的时候,直接调用 http.createHttp().request(url, option, callback),把 URL、请求方法、头信息、回调函数都写在一个地方。需要请求的地方多了,就多写几份。
方案B:封装一个统一请求方法
把请求逻辑抽成一个独立的函数或者类,统一处理 URL 前缀、token 注入、超时设置、错误格式化这些事情。每次发起请求,只需要调用这个封装好的方法,传入接口名称和参数就行。
我们来看个对比:
| 维度 | 方案A:直接用工具包 | 方案B:封装统一请求方法 |
|---|---|---|
| 学习成本 | 需要熟悉官方工具包文档 | 一次封装,多次复用,上手更顺滑 |
| 代码复用 | 每个页面都要写重复的请求逻辑 | 抽取公共方法,代码更简洁 |
| 错误处理 | 每个接口单独处理 | 统一拦截错误,逻辑集中 |
| 依赖程度 | 强依赖官方接口版本 | 封装层屏蔽底层变化 |
| 适用场景 | 简单的一次性请求 | 复杂应用、多接口协作 |
| 维护成本 | 高(改动一处要改很多处) | 低(统一入口,改一处全生效) |
| 可测试性 | 难以单独测试请求逻辑 | 可以单独测试封装层 |
3.2 我们为什么选方案B
想象一下你开了一家餐厅,方案A就像每个厨师自己跑去菜市场买菜——有人买了土豆,有人买了番茄,谁也不知道今天库存有什么。方案B则是有一个专职的采购员,所有食材都从他那里走,他要什么菜就去找供应商。
在应用里,这个"采购员"就是封装好的请求方法。好处是:
- URL 前缀统一管理。如果后端地址变了(从
https://api.v1.com换成https://api.v2.com),只需要改一个地方。 - token 自动注入。所有请求需要带的认证 token,在封装层统一加到 header 里,不用每个接口单独写。
- 错误格式统一。后端返回的错误码,在封装层统一转换成前端能理解的错误信息,展示给用户。
- 日志统一。每次请求的耗时、状态,在封装层统一打日志,方便排查问题。
3.3 具体选型理由
我们选择 HarmonyOS 官方的 @kit.NetworkKit,而不是第三方库,原因也很直接:
- 官方维护,稳定性有保障。ArkUI 版本更新时,官方工具包的兼容性是经过验证的。
- 权限体系打通。用官方工具包,系统在权限校验的时候更好处理。
- 不需要额外引入依赖。不需要 npm install,不需要担心第三方库版本冲突。
接下来我们就基于 @kit.NetworkKit 来写请求代码。
四、完整代码实现
这里我们把完整的代码拆成几块来讲,每一块只盯住一个核心目标。代码量不多,但每行都有它的作用。
4.1 发起网络请求
首先,核心的 HTTP 请求逻辑:
// UserService.ets
import { http } from '@kit.NetworkKit'
import { BusinessError } from '@kit.BasicServicesKit'
// 从网络获取用户资料
function fetchUserProfile(username: string): Promise<UserProfile> {
const url = `https://api.github.com/users/${username}`
const req = http.createHttp()
const option: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
header: {
'User-Agent': 'AtomGit-App',
},
expectDataType: http.HttpDataType.OBJECT
}
return new Promise((resolve, reject) => {
req.request(url, option, (err: BusinessError, data: http.HttpResponse) => {
if (err.code !== 0) {
reject(new Error(`网络请求失败: ${err.message}`))
return
}
const raw = data.result as GitHubUser
resolve(convertToUserProfile(raw))
})
})
}
这段代码做了一件什么事呢?想象一下你发微信:先编辑好内容(option 里定义请求方式、头信息),然后点击发送(req.request),对方回复之后,你再决定是处理消息还是告诉朋友"发失败了"。这里的 Promise 就是那个"等回复"的机制——发出去之后不卡在原地,先去做别的事,服务器回话了再来处理。
有几个地方值得特别说明一下:
http.RequestMethod.GET:明确指定是 GET 请求。HarmonyOS 的 HTTP 工具支持 GET、POST、PUT、DELETE 等常见方法。'User-Agent': 'AtomGit-App':GitHub API 要求所有请求都带 User-Agent,否则会返回 403 错误。这是个很容易被忽略的坑。expectDataType: http.HttpDataType.OBJECT:设置为 OBJECT 之后,返回的data.result会自动被解析成 JS 对象,不需要你手动JSON.parse。
4.2 展示用户卡片
有了数据来源,接下来要把它显示到界面上。我们把用户信息的展示封装成一个独立的组件 UserCard,这样代码更清晰,也方便复用:
// UserCard.ets
@Component
export struct UserCard {
@Prop user: UserProfile
build() {
Column() {
Image(this.user.avatarUrl)
.width(80).height(80)
.borderRadius(40)
.alt($r('sys.media.app_icon'))
Text(this.user.login)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top: 16 })
Text(`仓库: ${this.user.publicRepos} | 粉丝: ${this.user.followers}`)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 })
if (!this.user.bio.includes('很懒')) {
Text(this.user.bio)
.fontSize(13)
.fontColor('#888888')
.margin({ top: 10 })
.textAlign(TextAlign.Center)
.padding({ left: 20, right: 20 })
}
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
这里有一个小细节:@Prop 接收的是外部传入的数据,和 @State 不同,它是从父组件"单向流动"过来的。就像你从领导那里领任务,领到的是只读副本,改不动原数据。这种设计保证了数据流向清晰,不容易出现"页面悄悄改了状态,自己却不知道"的问题。
Image 组件里的 .alt() 是当图片加载失败时显示的占位图,这样用户体验会好一些,不会看到一张破图图标。
4.3 主页面:串联请求和展示
最后,把请求和展示串起来,这是整个页面的入口:
// Index.ets
import promptAction from '@ohos.promptAction'
@Entry
@Component
struct Index {
@State user: UserProfile | null = null
@State isLoading: boolean = false
@State errorMsg: string = ''
aboutToAppear(): void {
this.loadUser('atomgit')
}
loadUser(username: string): void {
this.isLoading = true
this.errorMsg = ''
fetchUserProfile(username)
.then((data: UserProfile) => {
this.user = data
this.isLoading = false
})
.catch((err: Error) => {
this.errorMsg = err.message
this.isLoading = false
promptAction.showToast({ message: `加载失败: ${err.message}` })
})
}
build() {
Column() {
if (this.isLoading) {
Column() {
LoadingProgress().width(50).height(50)
Text('正在加载用户信息...').fontSize(14).margin({ top: 12 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
} else if (this.errorMsg) {
Column() {
Text('⚠ 加载出错')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
Text(this.errorMsg).fontSize(13).fontColor('#999999')
Button('重新加载').onClick(() => this.loadUser('atomgit'))
.margin({ top: 16 })
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
} else if (this.user) {
UserCard({ user: this.user as UserProfile })
Button('换一个用户试试')
.onClick(() => this.loadUser('github'))
.margin({ top: 20 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
.padding(20)
}
}
几个关键点说明一下:
aboutToAppear 是在页面即将显示的时候自动调用的生命周期函数——我们用它来触发"页面一出来就加载数据"。这比把请求代码写在 build() 里要好,因为 build() 可能会被多次调用(比如状态变化时重新渲染),如果把请求也写在里面,就会重复发请求。
LoadingProgress 是 HarmonyOS 内置的加载动画组件,不需要自己写,非常方便。只需要在合适的状态下显示或隐藏它就好了。
promptAction.showToast 是HarmonyOS 提供的轻量级提示工具,用来在底部弹出一个短暂的提示文字,比 AlertDialog 打扰性小很多,适合用于错误提示。
五、深度技术原理
5.1 HTTP 请求的完整旅程
当你调用 http.createHttp().request(url, option) 时,背后发生了什么呢?让我们把整个过程拆开了看。
第一步:DNS 解析。 你的手机拿到 api.github.com 这个域名之后,要把它转换成 IP 地址。这需要访问 DNS 服务器,一般在 10~50 毫秒之间完成。如果 DNS 被劫持或者服务器 DNS 响应慢,这一步就会成为瓶颈。
第二步:建立 TCP 链接。 拿到 IP 地址之后,手机和 GitHub 服务器之间要建立一条可靠的传输通道,这就是 TCP 三次握手。这一步大约需要几十毫秒到一百毫秒不等,取决于网络质量。如果用户用的是移动网络,这一百毫秒可能是常态。
第三步:TLS 握手(HTTPS 专属)。 因为 GitHub 用的是 HTTPS,在 TCP 之上还要做一次 TLS 安全握手,确认双方的身份、协商加密算法、交换密钥。这一步大约再增加几十毫秒。所以 HTTPS 相对于 HTTP,连接建立时间要多出大约 100 毫秒。
第四步:发送 HTTP 请求。 链接建立好了,把你的请求报文塞进去发出去。这个报文里包含了请求方法(GET/POST 等)、URL 路径、请求头(Headers)、以及可选的请求体(Body)。报文本身通常很小,几十到几百字节,发送时间可以忽略不计。
第五步:服务器处理。 这是最不可控的一步。GitHub 的服务器收到请求,查数据库,组装 JSON 响应。在负载高的时候,服务器可能还要排队等 CPU。这个时间从几十毫秒到几秒不等,是整个请求链路上最不确定的一环。
第六步:接收响应数据。 服务器把响应发回来,手机一点一点接收。这个过程是流式的——数据不是一下子全到,而是一块一块来。expectDataType: http.HttpDataType.OBJECT 这个参数告诉 HarmonyOS:“等数据收完了再通知我,中途不用管。”
第七步:解析数据。 数据收齐之后,按照你指定的类型做解析。如果是 OBJECT 类型,HarmonyOS 会自动调用 JSON.parse 把字符串转成对象,然后回调你的函数。
整个过程加起来,短则 200 毫秒(局域网),长则 3~5 秒(弱网+跨国)。所以我们必须用异步的方式来写请求代码,否则整个界面会卡死在等待的这几秒钟里,用户看到的就是一个完全无响应的 App——这绝对不是你想要的结果。
5.2 同步写法 vs 异步写法:图解对比
如果用同步的方式写请求,大概是这样:
// 这种写法在 HarmonyOS 里是不存在的,但可以理解它的意思
const result = http.requestSync(url) // 卡住!界面不动!
this.user = result
用户看到的是:打开 App → 整个界面完全卡死 3 秒 → 数据突然蹦出来。这在体验上非常糟糕,尤其是弱网环境下,卡个 10 秒钟用户可能直接把你的 App 卸载了。
用异步的方式写:
fetchUserProfile(username).then(data => {
this.user = data // 数据回来了,更新界面
})
用户看到的是:打开 App → 看到加载动画 → 数据来了,动画消失,用户卡片出现。整个过程界面是流畅的,用户始终能感知到 App 在工作。
这就是为什么 HarmonyOS 的 HTTP 工具包从设计上就是基于回调和 Promise 的——它强制你用异步的方式写代码,从语言层面就堵死了同步卡界面这条路。
5.3 Promise 是什么,为什么用它
刚才代码里我们用的是 Promise 风格:
return new Promise((resolve, reject) => {
req.request(url, option, (err, data) => {
if (err.code !== 0) {
reject(new Error('出错了'))
} else {
resolve(data)
}
})
})
为什么要包这一层?因为 HarmonyOS 的 HTTP 模块本身是基于回调函数的。回调函数的写法像这样:
req.request(url, option, (err, data) => {
// 在这里处理结果
})
回调函数本身没问题,但如果请求多了——比如先拿用户信息、再拿用户仓库列表、再拿仓库的详情——代码会变成这样:
req.request(url1, option, (err1, data1) => {
req.request(url2, option, (err2, data2) => {
req.request(url3, option, (err3, data3) => {
// 终于拿到所有数据了
})
})
})
这就是著名的"回调地狱"——一层套一层,缩进越来越深,错误处理要重复写,代码读起来简直是一种折磨。
Promise 把这个嵌套结构"拍扁"了,变成了链式写法:
fetchUser()
.then(data1 => fetchRepos(data1.login))
.then(data2 => fetchDetails(data2.id))
.then(data3 => display(data3))
.catch(err => showError(err))
每一行的意图都非常清晰:先做 A,再做 B,再做 C,出了错统一处理。这就是 Promise 的魅力——让异步代码看起来像同步代码一样线性可读。
5.4 async/await:Promise 的语法糖
如果你觉得 .then().catch() 链还是有点绕,还有一个更简洁的写法:async/await。它让异步代码读起来几乎和同步代码一模一样:
async loadUserAndRepos(username: string): Promise<void> {
try {
const user = await fetchUserProfile(username) // 等这个完成
const repos = await fetchUserRepos(username) // 等这个完成
this.user = user
this.reposList = repos
} catch (err) {
this.errorMsg = (err as Error).message
}
}
await 关键字的意思是"在这里暂停,等 Promise 完成再继续往下走"。表面上看起来像是在等,但其实底层还是异步的——界面不会被卡住。async/await 只是 Promise 的语法糖,写起来更符合人写代码的自然思维。
5.5 @State 和 @Prop 的数据流原理
ArkUI 是一个声明式 UI框架。声明式的意思是:你描述"界面应该长什么样",框架负责"怎么把它渲染出来"以及"什么时候需要更新"。
打个比方:传统命令式编程像你在画布上自己一笔一笔画,每次有变化你都要判断"哪里需要重画",很麻烦。声明式 UI 像你给画家一张画像描述(“画一个红头发的人,戴眼镜”),画家(框架)自动决定怎么画、什么时候需要重画、你不需要管重画的技术细节。
在 ArkUI 里:
@State装饰的变量:组件自己持有、自己修改的"私有状态"。一旦它的值变了,框架会自动触发这个组件的重新渲染——所有用到这个变量的 UI 都会更新。@Prop装饰的变量:从父组件传进来的"属性",是只读的。子组件收到的是一个值的副本,不能直接改它。
在 Index.ets 里,user 是 @State,因为它需要在加载完成后被赋值,然后触发界面重新渲染。在 UserCard.ets 里,user 是 @Prop,因为卡片组件只负责展示数据,不负责修改数据。这种单向数据流的约束让应用的状态变得可预测——你永远知道数据是从哪里来、谁有权修改它。
六、常见问题解答
Q1:请求报 “Network error”,但我网络明明正常,是怎么回事?
这种情况最常见的原因是权限没有配置。HarmonyOS 应用要访问网络,必须在 module.json5 里声明网络权限,否则系统会直接拒绝你的请求。检查一下你的项目里有没有这个配置:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
没有这条配置,App 根本没有权限访问网络,连 DNS 解析都做不了,根本走不到发请求那一步。如果权限配了还报错,再依次检查:目标 URL 是否正确、服务器是否真的可达、是不是 VPN 把某些域名墙掉了。
Q2:接口返回的数据字段和我写的不一样,比如 GitHub 的 avatar_url 我想叫 avatarUrl,怎么处理?
在解析响应的回调里做一次字段映射就好。就像我们在 convertToUserProfile 函数里做的那样:
const result = data.result as GitHubUser
resolve({
avatarUrl: result.avatar_url, // API字段 -> 代码字段
publicRepos: result.public_repos,
bio: result.bio ?? '这个人很懒,什么都没写'
})
这样做有两个好处:第一,代码里统一用驼峰命名,后端 API 的下划线命名风格不会渗透到业务逻辑里;第二,万一哪天后端接口改了字段名(比如把 avatar_url 改成了 avatarUrl),你只需要改 convertToUserProfile 这一个函数,其他所有用到 avatarUrl 的地方不用动。
Q3:我想给请求加 token 认证(比如调用需要登录才能用的接口),要怎么做?
在请求头里加 Authorization 字段就好:
const option: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
header: {
'User-Agent': 'AtomGit-App',
'Authorization': 'Bearer ghp_xxxxxxxxxxxxxxxxxxxx' // 加这一行
}
}
不过,token 不要硬编码在代码里。虽然代码写起来最简单,但别人反编译你的 App 就能直接看到 token,然后就可以用你的身份调用接口,后果不堪设想。正确的做法是从你自己的后端服务器获取 token,或者使用 HarmonyOS 提供的 cryptoFramework 和安全存储 API,把 token 加密存在设备的安全区域。GitHub 的公开接口(只读用户信息)其实不需要 token,但如果你要操作私有仓库、提交代码、或者调用频率比较高的接口,就需要认证了。
Q4:请求有时候很慢,有时候干脆超时了,超时了怎么处理?
http.createHttp() 支持单独设置超时时间:
const option: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
connectTimeout: 8000, // 建联超时:8秒内连不上就放弃
readTimeout: 12000 // 读数据超时:12秒内收不到数据就放弃
}
connectTimeout:TCP 链接建立和 TLS 握手的时间上限。如果超过 8 秒还没建立好连接,就认为这次请求失败了。readTimeout:从发出请求到收到完整响应的时间上限。如果服务器很慢,12 秒内还没返回数据,就认为失败了。
超时会进入 .catch() 分支。你可以在那里给用户一个友好的提示,比如"网络有点慢,请稍后再试",同时提供一个重试按钮。
Q5:我要连续发两个请求,第二个依赖第一个的结果,要怎么写?
用 async/await 把两个请求串起来就好了:
async loadFullUserInfo(username: string): Promise<void> {
try {
this.isLoading = true
const user = await fetchUserProfile(username) // 先拿到用户信息
const repos = await fetchUserRepos(username) // 再拿仓库列表
this.user = user
this.reposList = repos
this.isLoading = false
} catch (err) {
this.isLoading = false
this.errorMsg = (err as Error).message
}
}
await 的神奇之处在于:它会"停在这里等",但整个应用不会卡——其他事情照常进行,只有这段代码在等第一个请求完成。等 fetchUserProfile 拿到数据了,才继续执行下一行 fetchUserRepos。这就是链式等待——前一个好了再下一个,不浪费。
如果你两个请求之间没有依赖(同时拿用户信息和拿公告列表),可以用 Promise.all 并行发出去:
async loadBoth(): Promise<void> {
const [user, announcements] = await Promise.all([
fetchUserProfile('atomgit'),
fetchAnnouncements()
])
this.user = user
this.announcements = announcements
}
Promise.all 会在两个请求都完成之后再继续,比串行发两个请求快很多。
Q6:接口返回的数据里有 null,页面展示的时候崩溃了,怎么办?
这是最常见的运行时错误之一。API 字段可能是 null 的情况比你想象的多——用户没填简介、bio 就是 null;用户没有公开仓库,public_repos 可能是 null。
给所有可能为空的字段加默认值是最稳妥的做法:
bio: (raw.bio as string | null) ?? '这个人很懒,什么都没写'
followers: (raw.followers as number | null) ?? 0
在模板里也可以用条件渲染做兜底:
if (this.user && this.user.bio) {
Text(this.user.bio).fontSize(13)
}
原则就是:永远不要假设 API 返回的数据是完整的。网络波动、接口变更、字段改名、版本迁移,任何一个环节都可能让你拿到一个 null 或者 undefined。提前兜底比事后修崩溃要省心得多,而且修复线上崩溃还需要用户更新 App,影响面更大。
七、运行效果
我们来看一下这个页面在不同状态下的实际效果。
7.1 页面打开,正在加载中

LoadingProgress 是 HarmonyOS 内置的加载动画,会自动旋转,你不需要写任何动画代码,只需要在 build() 里根据状态渲染它就好了。
7.2 数据加载成功,展示用户卡片

头像显示的是圆形裁剪后的 GitHub 用户头像。用户名用粗体大字突出展示,下面是统计数字,最下面是个人简介。点击按钮可以换成另一个用户(这里演示用的是 GitHub 官方账号)。
7.3 网络出错,显示错误提示

错误状态下的提示要尽量对用户友好。直接展示错误码 err.code: -1 之类的技术信息对普通用户来说毫无意义,要翻译成"网络连接失败,请检查网络设置"这样的自然语言。如果可能的话,给出下一步操作(比如重试按钮),让用户知道该怎么办。
八、扩展方向
学会了拉取一个用户的信息,恭喜你完成了 HarmonyOS 网络请求的入门。接下来可以从以下几个方向继续探索,每一个方向都对应着实际开发中常见的场景。
8.1 列表页:展示多个用户,支持分页
把单个用户卡片扩展成列表页,用 @ForEach 循环渲染多张用户卡片。这需要调用 GitHub 的搜索用户接口:https://api.github.com/search/users?q=关键词,它返回一个用户数组。
但这里要特别注意的是分页。GitHub API 每次最多返回 30 条数据,如果用户有 1000 个粉丝,不可能一次性全拉下来。正确的做法是:
- 先拉前 30 条,展示到列表里
- 用户下拉或者滚动到底部的时候,再拉第 31~60 条
- 把新数据追加到现有列表后面,而不是覆盖
这就涉及到一个重要的 UI 模式:下拉刷新 + 上拉加载更多。HarmonyOS 的 List 组件有 onReachEnd 事件可以监听滚动到底部,配合加载状态的管理,可以实现流畅的分页体验。
8.2 搜索功能:按关键词实时查找用户
加一个搜索框,用户输入用户名,实时发送搜索请求。这里有一个性能陷阱:如果用户输入 “a” 就发一次请求,输入 “ab” 又发一次,输入 “abo” 又发一次——用户还没打完字,你已经发了三条请求了,而且后面的请求可能比前面的先返回,导致列表顺序乱掉。
解决方案是防抖(debounce):用户停止输入 300 毫秒后才发请求。如果用户还在继续输入,就取消上一次的计划,重新计时。这样用户打完一个名字,实际上只发了一次请求,大大减轻了服务器压力。
// 防抖逻辑示意
let timer: number | null = null
function onSearchInput(name: string): void {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
this.loadSearch(name)
}, 300)
}
8.3 本地缓存:避免重复请求同一个数据
同一个用户的信息可能在一次会话里被访问多次。比如用户从列表页点进详情页看了一遍,又退回来,再点进去——如果每次都重新发请求,既浪费流量,体验也不好(用户要多等一轮时间)。
解决方案是加一层内存缓存:
const cache = new Map<string, UserProfile>()
function fetchUserProfile(username: string): Promise<UserProfile> {
if (cache.has(username)) {
return Promise.resolve(cache.get(username)!)
}
return realFetch(username).then(data => {
cache.set(username, data)
return data
})
}
加了缓存之后,第一次请求走真实网络,请求结果存入 Map;后续请求直接读 Map,返回速度几乎是即时的。
如果想要持久化缓存(App 重启之后也能用),可以配合 HarmonyOS 的 persistent 或者 preferences 实现,把数据存到本地文件里。
8.4 完整的错误分类处理
现在的错误处理比较简单,就是一个通用的"网络请求失败"。实际项目中,错误可以分为很多种,每种的处理方式也不一样:
- 网络不可达:提示"请检查网络连接",引导用户去设置页
- 请求超时:提示"加载超时,请稍后重试",自动触发一次重试
- 401 未认证:token 过期,跳转登录页面
- 403 权限不足:提示"没有权限访问此内容"
- 404 资源不存在:提示"未找到该用户"
- 500 服务器内部错误:提示"服务器开小差了,请稍后再试",自动记录日志
把这些错误分门别类地处理,用户体验会好很多——不同的问题有不同的应对方式,而不是每次都弹同一个"出错了"。
8.5 请求拦截与统一封装
当应用里有十几个甚至几十个接口的时候,每个接口都单独写一遍 header、token、超时配置,维护成本会急剧上升。更好的做法是用请求拦截器统一处理:
- 自动给每个请求加上
Authorization: token xxx - 自动给请求参数加上时间戳防止缓存
- 统一处理 401 错误,自动跳转登录页
- 统一打印请求日志,方便调试
这些都可以在封装层实现,业务代码完全不感知。对于大型应用来说,这套封装是必备的基础设施。
附:一个完整的工具函数封装示例
在实际项目中,我们通常会把所有请求相关的逻辑集中到一个 httpClient 文件里,这样整个应用的请求入口是统一的。下面给出一个更完整的封装思路,供大家参考:
// httpClient.ets - 统一请求封装
import { http } from '@kit.NetworkKit'
// 统一错误处理函数
function handleHttpError(code: number, message: string): never {
switch (code) {
case 401:
throw new Error('登录已过期,请重新登录')
case 403:
throw new Error('没有权限访问该资源')
case 404:
throw new Error('请求的资源不存在')
case 429:
throw new Error('请求太频繁了,请稍后再试')
default:
throw new Error(`请求失败: ${message}`)
}
}
// 通用的 GET 请求封装
function httpGet<T>(url: string, token?: string): Promise<T> {
const req = http.createHttp()
const headers: Record<string, string> = {
'User-Agent': 'AtomGit-App',
'Accept': 'application/json'
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const option: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
header: headers,
connectTimeout: 10000,
readTimeout: 15000,
expectDataType: http.HttpDataType.OBJECT
}
return new Promise((resolve, reject) => {
req.request(url, option, (err, data) => {
req.destroy()
if (err.code !== 0) {
handleHttpError(err.code, err.message)
return
}
resolve(data.result as T)
})
})
}
export { httpGet }
这段封装里有一个小细节:req.destroy()。HarmonyOS 的 HTTP 请求对象是有状态的,每次请求完成后如果不销毁,下次请求可能会复用上一次的状态,导致一些莫名其妙的问题(比如 header 混乱)。养成在回调里立即调用 destroy() 的习惯,是一个非常好的实践。
如果你在写一个团队项目,建议把这套封装方案做成一个独立的 HAR(Harmony Archive)包,让多个模块共享。这样每个开发者在自己的页面里只需要调用 httpGet<UserProfile>(url),完全不用关心底层的请求细节——URL 前缀、超时配置、token 管理这些都统一处理好了。
更多推荐



所有评论(0)