HarmonyOS技术精讲-Network Kit(网络服务)| 01 初识HTTP请求:从零开始获取数据

从一个困惑点开始
很多人在 HarmonyOS NEXT 开发里第一次接触 HTTP 请求时,会遇到一个尴尬的情况:官方示例能跑通,返回的 JSON 也能打印出来,但下一步就卡住了——拿到数据之后怎么传给 UI?这部分官方文档写得很克制,点到为止。
另一个常见问题是:请求发出去之后,页面返回了,但请求回调还在执行,导致状态更新到已销毁的组件上,直接报 undefined 错误。
这篇文章不讲空话,直接通过一个完整的新闻列表加载功能,把 @ohos.net.http 的完整链路走一遍。包括创建请求、设置头/体、解析响应、Cookie 处理,以及上面提到的坑怎么绕过去。
它解决什么问题
HTTP 数据请求是移动端最基础的能力。无论是登录、加载列表、提交表单,底层走的都是 HTTP/HTTPS 协议。
@ohos.net.http 这个模块提供的是 HarmonyOS 原生网络请求能力,不依赖第三方库。它解决的问题很明确:在 ArkTS 环境下发起网络请求,获取服务端数据。
适合场景:
- 常规 RESTful API 调用(JSON 格式)
- 文件上传/下载(需要额外配置)
- Cookie 需要持久化的场景
不适合场景:
- WebSocket 长连接(这个应该用
@ohos.net.webSocket) - 流式响应处理(需要自己实现分块读取)
和第三方库(比如 axios)对比,@ohos.net.http 的优势是不需要额外引入依赖,项目初始化就能用,且对底层网络栈的控制更直接。缺点是 API 设计偏底层,缺少一些语法糖,比如没有自动序列化、拦截器等。
| 对比项 | @ohos.net.http | 第三方库(如 axios 的移植版) |
|---|---|---|
| 依赖引入 | 无 | 需要单独引入 |
| 拦截器 | 无 | 有 |
| 自动 JSON 解析 | 无 | 有 |
| Cookie 管理 | 原生支持 | 需要额外处理 |
| 性能 | 原生调用 | 额外封装层 |
对于大多数业务场景,原生模块已经完全够用。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心实现:新闻列表加载
假设接口返回的数据格式如下:
{
"code": 200,
"data": [
{ "id": 1, "title": "标题1", "source": "来源A", "time": "2024-01-01" },
{ "id": 2, "title": "标题2", "source": "来源B", "time": "2024-01-02" }
]
}
第一步:创建 HTTP 请求并获取数据
这一段主要用于封装网络请求逻辑。把它单独抽成一个文件,方便复用。
// src/main/ets/common/http/NewsHttp.ts
import http from '@ohos.net.http';
import { BusinessError } from '@kit.BasicServicesKit';
// 定义响应数据结构
export interface NewsItem {
id: number;
title: string;
source: string;
time: string;
}
export interface NewsResponse {
code: number;
data: NewsItem[];
}
export class NewsHttp {
private httpRequest: http.HttpRequest;
constructor() {
// 创建 HTTP 请求对象,一个请求任务只使用一次,用完必须销毁
this.httpRequest = http.createHttp();
}
async fetchNews(): Promise<NewsResponse> {
try {
// 配置请求参数
const url = 'https://api.example.com/news/list';
const requestOption: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
connectTimeout: 10000, // 连接超时 10 秒
readTimeout: 10000, // 读取超时 10 秒
usingProtocol: http.HttpProtocol.HTTP1_1, // 使用 HTTP/1.1
};
// 发起请求
const response: http.HttpResponse = await this.httpRequest.request(url, requestOption);
// 检查响应状态码
if (response.responseCode !== 200) {
throw new Error(`请求失败,状态码: ${response.responseCode}`);
}
// 解析 JSON 数据
const result: NewsResponse = JSON.parse(response.result as string);
return result;
} catch (error) {
// 统一捕获网络错误和解析错误
const err = error as BusinessError;
console.error(`新闻请求失败: ${err.message}`);
throw error;
} finally {
// 无论成功失败,都必须销毁请求对象,释放资源
this.httpRequest.destroy();
}
}
}
注意事项:
http.createHttp()创建的请求对象是单次任务绑定的,每次请求完成后必须调用destroy(),否则持续占用资源,可能导致内存泄漏。connectTimeout和readTimeout的默认值可能不够用,建议根据业务场景显式设置。默认值在低信号环境下容易超时。response.result的类型是Object,解析时需要注意。如果接口返回的是非 JSON 格式,JSON.parse会报错。
第二步:在页面中调用并渲染
页面组件负责管理 UI 状态和调用网络请求。
// src/main/ets/pages/NewsListPage.ets
import { NewsHttp, NewsItem, NewsResponse } from '../common/http/NewsHttp';
@Entry
@Component
struct NewsListPage {
@State newsList: NewsItem[] = [];
@State isLoading: boolean = true;
aboutToAppear() {
this.loadNews();
}
async loadNews() {
this.isLoading = true;
try {
const httpClient = new NewsHttp();
const response: NewsResponse = await httpClient.fetchNews();
// 拿到数据后更新状态,触发 UI 刷新
this.newsList = response.data;
} catch (error) {
console.error(`加载新闻失败: ${error.message}`);
// 这里可以加入 Toast 提示,但不影响 UI 渲染
} finally {
this.isLoading = false;
}
}
build() {
Column() {
if (this.isLoading) {
Progress({ value: 0, type: ProgressType.Linear }).width('80%').margin(20);
} else if (this.newsList.length === 0) {
Text('暂无新闻数据').margin(20);
} else {
List({ space: 12 }) {
ForEach(this.newsList, (item: NewsItem) => {
ListItem() {
Column() {
Text(item.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%');
Row() {
Text(item.source).fontSize(14).fontColor('#666');
Text(item.time).fontSize(14).fontColor('#999');
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 });
}
.padding(12)
.width('100%')
.backgroundColor('#F5F5F5')
.borderRadius(8);
}
}, (item: NewsItem) => item.id.toString());
}
.width('100%')
.padding(12);
}
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF');
}
}
关键点:
aboutToAppear是在页面显示之前调用的生命周期,适合发起初始数据请求。@State修饰的变量变化会自动触发 UI 重新渲染,不需要手动调用刷新接口。ForEach中的键值必须唯一且稳定,否则列表会出现闪烁或重复渲染。这里用item.id.toString()作为键。
第三步:处理 POST 请求和 Cookie
上面是 GET 请求,实际项目中 POST 也常见,尤其是登录场景。
// src/main/ets/common/http/LoginHttp.ts
import http from '@ohos.net.http';
import { BusinessError } from '@kit.BasicServicesKit';
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
token: string;
}
export class LoginHttp {
private httpRequest: http.HttpRequest;
constructor() {
this.httpRequest = http.createHttp();
}
async login(params: LoginRequest): Promise<LoginResponse> {
try {
const url = 'https://api.example.com/login';
const requestOption: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json'
},
extraData: JSON.stringify(params), // POST 请求体,必须是字符串
// 使用默认 CookieStore,会自动管理 Session
usingCookie: true,
};
const response = await this.httpRequest.request(url, requestOption);
if (response.responseCode !== 200) {
throw new Error(`登录失败,状态码: ${response.responseCode}`);
}
// 手动设置 Cookie,后续请求带上
const cookies = response.cookies;
if (cookies && cookies.length > 0) {
// CookieStore 是全局的,会自动处理
http.CookieStore.setCookie(url, cookies[0]);
}
const result: LoginResponse = JSON.parse(response.result as string);
return result;
} catch (error) {
const err = error as BusinessError;
console.error(`登录请求失败: ${err.message}`);
throw error;
} finally {
this.httpRequest.destroy();
}
}
}
关于 Cookie 管理的几个点:
usingCookie: true表示开启自动 Cookie 管理。默认使用全局的CookieStore,所有请求共享 Cookie。- 如果只是登录后获取 Cookie,全局 CookieStore 会自动存储,后续请求不需要手动设置。
- 如果服务端返回了
Set-Cookie,response.cookies数组里会包含值,手动调用CookieStore.setCookie也可以,但全局模式下会自动处理。 - 特别提醒:
CookieStore是当前进程内的,应用重启后 Cookie 会丢失。如果需要持久化,需要自己存到Preferences或数据库里。
常见问题与踩坑记录
问题 1:请求任务未及时销毁导致内存泄漏
现象:页面反复打开/关闭,内存持续增长,最终 OOM。
原因:http.createHttp() 每次调用都会创建一个底层 Socket 连接,如果不调用 destroy(),这些连接会一直保持,直到进程结束。
解法:
- 严格按照“创建-使用-销毁”的生命周期管理。在
finally块里调用destroy(),确保无论成功还是出错都会释放。 - 如果使用类封装,在类的析构函数或页面
aboutToDisappear里也加上destroy()兜底。
aboutToDisappear() {
// 页面销毁时,确保请求对象被释放
if (this.httpRequest) {
this.httpRequest.destroy();
}
}
问题 2:响应编码问题导致中文乱码
现象:接口返回的中文显示为 \uXXXX 或乱码。
原因:默认情况下,http.request 返回的 result 是 string 类型,但底层编码可能与预期不一致。
解法:
- 服务端响应头里加上
charset=utf-8,大多数情况下问题能解决。 - 如果需要强制指定解码,可以用
buffer模式接收,然后手动解码。但这个在@ohos.net.http里较复杂,建议优先让服务端配合。
// 服务端返回的 Content-Type 必须明确指定编码
requestOption.header['Content-Type'] = 'application/json; charset=utf-8';
问题 3:模拟器请求正常,真机失败
现象:在模拟器上能拿到数据,但真机上报 网络连接失败。
原因:真机需要网络权限,并且需要处理 HTTPS 证书验证问题。
解法:
- 检查
module.json5中是否配置了ohos.permission.INTERNET。 - 如果是自签名证书,需要配置
usingHttpDns或caPath。生产环境建议使用正规证书。 - 真机网络环境复杂,建议在真机上测试时关闭代理,或者使用抓包工具(如 Charles)时检查证书是否正确安装。
最佳实践
1. 不要在 build() 中创建 HTTP 请求对象
build() 方法在每次状态变化时都可能被重新调用,如果在里面创建请求对象,会导致大量无效的对象创建,增加内存压力。把网络请求封装到独立的 Service 类中,并在页面生命周期中调用。
2. 统一管理超时时间
每个接口的超时时间不一定相同。上传图片的超时时间应该比普通 GET 请求长。建议把超时时间配置到配置文件里,或者在 Service 类中作为参数传入。
// 推荐:把超时时间作为构造参数传入
constructor(timeout: number = 10000) {
this.httpRequest = http.createHttp();
this.timeout = timeout;
}
3. 使用 try-catch 包裹整个请求链路
网络请求是不可靠的。无论是网络断开、服务器异常,还是 JSON 解析失败,都应该被 try-catch 捕获,并给出友好的用户提示。不要在异步回调里直接修改 UI 状态,否则页面销毁后状态更新会报错。
FAQ
Q:为什么真机正常,模拟器不生效?
A:检查模拟器的网络代理设置。模拟器默认可能使用宿主机的代理,如果代理配置不正确,请求可能被拦截。建议在模拟器上关闭代理,或者直接使用真机测试。
Q:为什么页面返回后状态丢失?
A:@State 变量的生命周期和页面组件绑定。页面返回后组件被销毁,状态自然丢失。如果需要跨页面保持状态,可以使用 AppStorage 或者全局数据管理方案。
Q:为什么第一次授权成功,第二次失败?
A:检查 Cookie 是否正常传递。如果使用了 usingCookie: true,但在第一次请求后没有正确处理 Set-Cookie,第二次请求可能不会带上 Session ID。建议在登录成功后,检查 CookieStore 中是否确实存储了 Cookie。
Q:高并发场景下,多个请求同时创建 http.createHttp() 会不会有问题?
A:理论上每个请求对象是独立的,但底层 Socket 资源有上限。建议对并发数做限制,或者在请求完毕后立即释放资源。如果并发量非常大(比如超过 30 个同时请求),可以考虑使用请求队列。
Q:这个 API 支持 file 协议吗?
A:不支持。@ohos.net.http 只支持 HTTP/HTTPS 协议。本地文件读取应该使用 @ohos.file.fs 模块。
更多推荐



所有评论(0)