在这里插入图片描述

从一个困惑点开始

很多人在 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(),否则持续占用资源,可能导致内存泄漏。
  • connectTimeoutreadTimeout 的默认值可能不够用,建议根据业务场景显式设置。默认值在低信号环境下容易超时。
  • 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-Cookieresponse.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 返回的 resultstring 类型,但底层编码可能与预期不一致。

解法

  • 服务端响应头里加上 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
  • 如果是自签名证书,需要配置 usingHttpDnscaPath。生产环境建议使用正规证书。
  • 真机网络环境复杂,建议在真机上测试时关闭代理,或者使用抓包工具(如 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 模块。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐