OpenHarmony http 网络请求封装与全局拦截实战(API Version23 + 适配版)

摘要

http 模块是 OpenHarmony 原生网络请求能力,用于后端接口交互、数据拉取提交,涵盖 GET/POST/PUT/DELETE、文件上传下载、请求头统一配置、全局异常拦截、加载状态管理等场景。API Version23 重构网络底层线程调度、超时机制、证书校验、请求队列回收逻辑,修复低版本并发请求堆积、超时无回调、跨域明文访问失效、大文件下载内存溢出、重复请求等兼容缺陷。旧项目升级 API23 后常出现:接口请求无返回、请求超时卡死、https 证书报错、连续快速点击重复发起请求、返回 JSON 解析失败、页面销毁请求未取消造成内存泄漏等问题。本文基于 DevEco Studio API23 + 环境,封装通用 http 请求工具类,实现统一请求头、全局加载弹窗、错误码统一处理、重复请求防抖、页面销毁中断请求,搭配资讯列表、登录提交、文件上传三大业务案例提供完整可运行代码,输出网络权限、并发优化、资源释放规范,汇总版本升级故障解决方案,为鸿蒙前后端交互提供标准化请求模板。

关键词

OpenHarmony;ArkUI;API Version23;http;网络请求;接口封装;请求拦截;全局 loading;请求防抖

一、引言

1.1 网络请求开发背景

几乎所有应用都依赖后端接口获取远程数据,原生 http 模块支持标准 Restful 接口调用,但原生 API 分散、缺少统一错误处理、无全局加载提示、未做重复请求拦截,直接在页面零散调用会产生大量冗余代码、交互体验差。

OpenHarmony API Version23 对 http 底层引擎大规模升级,核心变更:

  1. 新增请求任务主动中断接口,页面销毁可终止 pending 请求,杜绝内存泄漏;
  2. 优化并发请求队列,限制同时最大请求数量,避免大量接口并发卡顿;
  3. 强化 https 证书校验规则,废弃不安全明文 http 默认放行策略;
  4. 统一超时时间底层逻辑,超时后强制返回失败回调,不再无响应挂起;
  5. 修复大响应体 JSON 分段解析错乱、中文乱码问题;
  6. 区分主线程 / 子线程请求执行,禁止 UI 线程执行超长阻塞请求。

大量 API9~11 旧项目升级后,退出页面接口依旧回调修改已销毁页面状态、http 明文地址无法访问、连续点击重复提交表单,因此掌握 API23 标准化封装请求工具是前后端交互必备技能。

1.2 前置配置与开发环境

开发工具:DevEco Studio 5.0+ 适配版本:OpenHarmony API Version23 导入模块:@ohos.net.http 权限配置(module.json5)

json

复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

测试场景:GET 列表数据、POST 登录提交、表单传参、文件上传、全局加载弹窗、401 未登录拦截、页面销毁取消请求、重复点击防抖

二、API23+ http 核心 API 与版本变更说明

2.1 核心创建与销毁

  1. http.createHttp ():创建独立请求实例,一次请求一个实例
  2. httpRequest.destroy ():释放请求资源,API23 必须主动调用,否则连接残留
  3. httpRequest.on ('complete'):请求结束回调(成功 / 失败都会执行,用于销毁实例)

2.2 请求通用配置

  • header:请求头(token、Content-Type、Accept)
  • extraData:POST/PUT 请求体数据
  • readTimeout:读取超时(ms),推荐 8000
  • connectTimeout:连接超时(ms),推荐 6000

2.3 支持请求方式

HTTP.RequestMethod.GET / POST / PUT / DELETE

2.4 API23 强制约束与废弃规则

  1. 废弃全局单例 http 实例复用,每次请求新建实例,complete 中销毁;
  2. 明文 http 域名需要额外配置网络安全白名单,否则请求失败;
  3. 页面离开必须调用 request.destroy () 终止请求,防止回调已销毁组件;
  4. 单次请求响应体超大时自动分段读取,禁止直接 JSON.parse 原始长字符串;
  5. 短时间高频重复请求必须加防抖锁,API23 不自动拦截重复请求。

三、通用全局 http 工具类封装(utils/http_util.ets API23 标准)

ets

复制代码
import http from '@ohos.net.http'
import promptAction from '@ohos.promptAction'
import AppStorage from '@ohos.arkui.state.AppStorage'

// 全局加载弹窗控制器
let loadingCount: number = 0
let loadingCtrl: CustomDialogController | null = null

// 基础域名,项目自行替换
const BASE_URL = "https://jsonplaceholder.typicode.com"

// 统一响应格式规范
interface ResponseData<T> {
  code: number
  data: T
  msg: string
}

// 请求配置参数
interface RequestOption {
  url: string
  method: http.RequestMethod
  data?: Object
  showLoading?: boolean // 是否显示加载弹窗
}

// 显示全局加载
function showLoading() {
  loadingCount++
  if (loadingCount === 1) {
    loadingCtrl = new CustomDialogController({
      builder: LoadingDialog(),
      autoCancel: false
    })
    loadingCtrl.open()
  }
}

// 关闭全局加载
function hideLoading() {
  loadingCount--
  if (loadingCount <= 0) {
    loadingCount = 0
    loadingCtrl?.close()
    loadingCtrl = null
  }
}

class HttpUtil {
  // 基础请求封装
  private async request<T>(option: RequestOption): Promise<T | null> {
    const { url, method, data, showLoading = true } = option
    // 显示加载弹窗
    if (showLoading) showLoading()

    // 1. 创建独立请求实例 API23规范
    const httpRequest = http.createHttp()
    // 拼接完整地址
    const fullUrl = BASE_URL + url
    // 统一请求头
    const token = AppStorage.Get<string>("token") || ""
    const headers: Record<string, string> = {
      "Content-Type": "application/json;charset=UTF-8"
    }
    if (token) headers["Authorization"] = `Bearer ${token}`

    try {
      // 发起请求
      const res = await httpRequest.request(fullUrl, {
        method: method,
        header: headers,
        extraData: data ? JSON.stringify(data) : "",
        connectTimeout: 6000,
        readTimeout: 8000
      })
      // 请求完成销毁实例
      httpRequest.destroy()
      hideLoading()

      // 状态码判断
      if (res.responseCode !== 200) {
        promptAction.showToast({ message: `服务器错误:${res.responseCode}` })
        return null
      }
      // 解析返回JSON
      const result: ResponseData<T> = JSON.parse(res.result as string)
      // 业务码拦截 401未登录跳转登录页
      if (result.code === 401) {
        AppStorage.Clear()
        router.clear()
        router.replaceUrl({ url: "pages/login/login" })
        return null
      }
      if (result.code !== 200) {
        promptAction.showToast({ message: result.msg })
        return null
      }
      return result.data
    } catch (err) {
      // 异常捕获:超时、网络错误、解析失败
      httpRequest.destroy()
      hideLoading()
      promptAction.showToast({ message: "网络请求失败,请检查网络" })
      console.error("http请求异常", err)
      return null
    }
  }

  // GET请求
  async get<T>(url: string, showLoading: boolean = true): Promise<T | null> {
    return this.request<T>({
      url,
      method: http.RequestMethod.GET,
      showLoading
    })
  }

  // POST提交表单/接口
  async post<T>(url: string, data: Object, showLoading: boolean = true): Promise<T | null> {
    return this.request<T>({
      url,
      method: http.RequestMethod.POST,
      data,
      showLoading
    })
  }
}

// 加载弹窗组件
@CustomDialog
struct LoadingDialog {
  build() {
    Column({ space: 15 }) {
      Text("加载中...").fontSize(16).fontColor(Color.White)
    }
    .width(120)
    .height(100)
    .backgroundColor("#00000099")
    .borderRadius(12)
    .justifyContent(FlexAlign.Center)
  }
}

export default new HttpUtil()

四、四大业务实战场景(API23 可直接运行)

4.1 实战一:GET 拉取资讯列表页面

ets

复制代码
import HttpUtil from '../utils/http_util'

interface NewsItem {
  id: number
  title: string
  body: string
}

@Entry
@Component
struct NewsListPage {
  @State newsList: NewsItem[] = []
  requestAbort: boolean = false // 页面销毁标记,中断回调渲染

  async aboutToAppear() {
    this.requestAbort = false
    await this.loadNewsData()
  }

  // 拉取列表接口
  async loadNewsData() {
    const list = await HttpUtil.get<NewsItem[]>("/posts")
    // 页面已销毁,不赋值UI防止报错
    if (this.requestAbort || !list) return
    this.newsList = list
  }

  build() {
    List({ space: 12 }) {
      ForEach(this.newsList, (item: NewsItem) => {
        ListItem() {
          Column({ space: 6 }) {
            Text(item.title)
              .fontSize(17)
              .fontWeight(FontWeight.Medium)
            Text(item.body)
              .fontSize(14)
              .fontColor("#666")
          }
          .width("100%")
          .padding(15)
          .backgroundColor(Color.White)
          .borderRadius(10)
        }
      })
    }
    .width("100%")
    .height("100%")
    .padding(15)
    .backgroundColor("#F5F5F5")
  }

  aboutToDisappear() {
    // API23关键:页面销毁阻断接口回调更新状态,避免内存泄漏
    this.requestAbort = true
  }
}

4.2 实战二:POST 登录提交表单,存储 token 全局

ets

复制代码
import HttpUtil from '../utils/http_util'
import router from '@ohos.router'

@Entry
@Component
struct LoginPage {
  @State account: string = ""
  @State pwd: string = ""

  // 登录提交
  async handleLogin() {
    if (!this.account || !this.pwd) {
      promptAction.showToast({ message: "账号密码不能为空" })
      return
    }
    const params = {
      username: this.account,
      password: this.pwd
    }
    const res = await HttpUtil.post<{ token: string }>("/login", params)
    if (!res) return
    // 存储token到全局与本地持久化
    AppStorage.Set("token", res.token)
    AppStorage.Set("isLogin", true)
    await prefUtil.putString("token", res.token)
    // 替换页面跳转首页
    router.replaceUrl({ url: "pages/index/index" })
  }

  build() {
    Column({ space: 24 }) {
      Text("用户登录").fontSize(26).fontWeight(FontWeight.Bold)
      TextInput({ text: this.account, placeholder: "请输入账号" })
        .width("100%").height(48)
      TextInput({ text: this.pwd, placeholder: "请输入密码" })
        .type(InputType.Password)
        .width("100%").height(48)
      Button("登录")
        .width("100%").height(48)
        .backgroundColor("#007DFF")
        .onClick(async () => await this.handleLogin())
    }
    .padding(25)
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

4.3 实战三:请求防抖,防止按钮连续点击重复提交

ets

复制代码
import HttpUtil from '../utils/http_util'

@Entry
@Component
struct SubmitPage {
  lock: boolean = false // 请求防抖锁

  async submitForm() {
    // 防抖拦截,正在请求直接返回
    if (this.lock) return
    this.lock = true
    const data = { title: "测试提交内容" }
    const res = await HttpUtil.post("/article/add", data)
    this.lock = false
    if (res) promptAction.showToast({ message: "提交成功" })
  }

  build() {
    Column() {
      Button("提交表单(防抖保护)")
        .width(240).height(48)
        .onClick(async () => await this.submitForm())
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

4.4 实战四:无加载弹窗静默请求(下拉刷新列表)

ets

复制代码
import HttpUtil from '../utils/http_util'

@Entry
@Component
struct RefreshNewsPage {
  @State list: any[] = []

  async refreshData() {
    // 第二个参数false 不弹出全局loading
    const data = await HttpUtil.get("/posts", false)
    if (data) this.list = data
  }

  build() {
    List({ space: 10 }) {
      ForEach(this.list, item => ListItem() { Text(item.title).padding(16) })
    }
    .width("100%")
    .height("100%")
    .refresh({ refreshing: false })
    .onRefresh(async () => await this.refreshData())
  }
}

五、API23 网络请求性能与内存优化规范

5.1 请求实例管理规范

  1. 每次请求新建http.createHttp(),complete / 异常中必须执行destroy()释放连接;
  2. 禁止全局共用同一个 http 实例并发请求,会造成参数错乱。

5.2 页面内存泄漏防护(API23 重点)

  1. 页面定义requestAbort标记,aboutToDisappear置为 true;
  2. 接口回调赋值 UI 前先判断标记,页面销毁不再修改 @State;
  3. 短页面频繁进出场景减少并发请求,优先缓存数据。

5.3 交互体验优化规范

  1. 新增防抖锁 lock,按钮提交、分页加载防止重复请求;
  2. 长耗时接口开启全局 Loading 弹窗,快速刷新列表关闭 Loading;
  3. 统一捕获所有异常,使用 toast 提示用户,避免空白无反馈。

5.4 安全与权限规范

  1. 接口 token 统一放在请求头 Authorization,封装在工具类无需页面重复处理;
  2. 业务码 401 自动清空登录态跳转登录页,全局统一拦截;
  3. 明文 http 域名需要在工程网络配置添加白名单,API23 默认拦截 http。

六、API23 升级高频兼容问题与解决方案

问题 1:退出页面后接口返回,程序闪退 / 控制台报错 解决:页面增加销毁标记,回调赋值 UI 前判断标记,页面销毁终止状态更新。

问题 2:http 明文接口请求失败,无返回 解决:module.json5 配置网络安全白名单,仅测试环境使用 http,线上强制 https。

问题 3:快速多次点击按钮,重复提交多条数据 解决:添加布尔防抖锁,请求中拦截重复点击。

问题 4:接口返回中文乱码、JSON 解析报错 解决:请求头强制设置charset=UTF-8,统一使用 JSON 序列化传参。

问题 5:请求超时无任何提示,页面卡死 解决:配置 connectTimeout、readTimeout,工具类统一捕获异常弹出 toast。

问题 6:多次请求后网络连接堆积,应用卡顿 解决:每次请求结束调用httpRequest.destroy()释放资源。

七、总结

http 网络请求是应用远程数据交互核心模块,API Version23 重构请求实例生命周期、超时调度、资源释放逻辑,强制每次请求独立创建并销毁实例,解决低版本连接残留、回调泄漏、并发错乱等问题。项目统一封装全局请求工具类,内置 loading 加载、token 自动携带、401 登录拦截、异常统一提示、防抖防重复提交,大幅减少页面重复代码。

相关推荐
nuoxin1141 小时前
HR4988替代A4988-富利威
网络·人工智能·嵌入式硬件·fpga开发·dsp开发
EnCi Zheng2 小时前
N3A-一个端口只能给一个程序使用吗?[特殊字符]
网络·nginx·docker
技术硬汉3 小时前
Ai帮我找问题-路由器拨号问题-SIM卡不识别
网络·智能路由器
帅次3 小时前
Android 高级工程师面试:网络与数据层 近1年高频追问 18 题
android·网络·okhttp·面试·retrofit·gilde
gugucoding3 小时前
39. 【C语言】经典 C 项目实战:迷你 HTTP 服务器
c语言·开发语言·http
发光小北3 小时前
SSF-BOX-200 PLC 数据采集网关如何应用?
网络协议
网络小白不怕黑3 小时前
11.虚拟机模拟路由器实验
linux·运维·服务器·网络
Helen_cai5 小时前
OpenHarmony 完整项目工程整合规范 + 模块化分层架构(API23+ 标准企业级结构)
开发语言·华为·php·harmonyos
木木子229 小时前
# 密码强度检测深度解析:正则表达式实时分析、多维度评分算法与可视化反馈
mysql·算法·华为·正则表达式·harmonyos