鸿蒙 ArkTS 网络请求实战:从 HTTP 到 Axios 封装,打造生产级请求层

鸿蒙 ArkTS 网络请求实战:从 HTTP 到 Axios 封装,打造生产级请求层


网络请求是每个 App 的标配,但很多鸿蒙开发者在这里卡得一塌糊涂------要么请求发出去没反应,要么 Token 管理乱成一锅粥,要么每个页面都在重复写 http.createHttp()

这篇文章从零开始,把鸿蒙原生 HTTP 模块的坑踩一遍,再一步步封装成可以直接用于生产的请求层,包含拦截器、Token 刷新、错误统一处理,代码拿走即用。


背景:鸿蒙网络请求有哪些选择?

目前 HarmonyOS NEXT 环境下,网络请求主要有两条路:

  1. 原生 @ohos.net.http 模块:系统内置,零依赖,适合简单场景
  2. @ohos/axios:官方支持的适配版本,API 风格和 Web 端一致,适合复杂项目

两者对比如下:

维度 原生 http @ohos/axios
依赖 需要 ohpm 安装
API 风格 回调/Promise Promise/async-await
拦截器
并发控制 手动 内置
推荐场景 简单接口、工具脚本 生产级应用

实际项目中,建议是:原生 http 了解就行,生产项目直接上 axios


第一步:用原生 HTTP 模块先跑通一个请求

打权限是第一道关卡,在 module.json5 里加上网络权限:

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

权限加好之后,用原生 http 发一个 GET 请求:

typescript 复制代码
// pages/Index.ets
import http from '@ohos.net.http';

@Entry
@Component
struct Index {
  @State message: string = '等待请求...'

  aboutToAppear() {
    this.fetchData()
  }

  async fetchData() {
    // 每次请求需要创建一个 httpRequest 实例
    const httpRequest = http.createHttp()

    try {
      const response = await httpRequest.request(
        'https://jsonplaceholder.typicode.com/posts/1',
        {
          method: http.RequestMethod.GET,
          header: {
            'Content-Type': 'application/json'
          },
          connectTimeout: 10000,  // 连接超时 10s
          readTimeout: 10000      // 读取超时 10s
        }
      )

      if (response.responseCode === 200) {
        // responseCode 是数字,不是字符串
        const data = JSON.parse(response.result as string)
        this.message = data.title
      } else {
        this.message = `请求失败: ${response.responseCode}`
      }
    } catch (err) {
      // err 是 BusinessError 类型
      this.message = `网络异常: ${(err as Error).message}`
    } finally {
      // ⚠️ 重要:用完必须销毁,否则内存泄漏
      httpRequest.destroy()
    }
  }

  build() {
    Column() {
      Text(this.message)
        .fontSize(16)
        .padding(20)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

几个容易踩的坑:

  • httpRequest.destroy() 必须调用,不然每次请求都会泄漏一个 httpRequest 实例
  • response.resultstring | ArrayBuffer,需要手动断言再解析
  • 超时要显式设置,默认超时时间很长,用户体验差

第二步:安装 @ohos/axios

安装之前,先查一下最新版本,避免装到过时版本:

bash 复制代码
# 查询最新可用版本
ohpm show @ohos/axios

# 安装
ohpm install @ohos/axios

安装完在 oh-package.json5 里确认有这个依赖:

json 复制代码
{
  "dependencies": {
    "@ohos/axios": "^2.2.0"
  }
}

注意:版本号以 ohpm show 查到的为准,本文使用 2.2.0 编写,后续版本 API 保持向后兼容。

验证安装成功,最基础的用法:

typescript 复制代码
import axios from '@ohos/axios'

const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1')
console.log(response.data.title)

第三步:封装 Token 管理模块

在封装请求层之前,先把 Token 存取的工具方法准备好,后续拦截器会用到。

typescript 复制代码
// utils/auth.ets
import preferences from '@ohos.data.preferences';
import common from '@ohos.app.ability.common';
import axios from '@ohos/axios';

const PREF_NAME = 'app_auth'
const TOKEN_KEY = 'access_token'
const REFRESH_TOKEN_KEY = 'refresh_token'

// 内存缓存,避免每次读磁盘
let _token: string = ''
let _refreshToken: string = ''

// 获取 access token(优先内存,其次读持久化)
export function getToken(): string {
  return _token
}

// 保存 token(同时写内存和持久化)
export async function saveToken(token: string, refreshToken: string, context: common.UIAbilityContext) {
  _token = token
  _refreshToken = refreshToken

  const prefs = await preferences.getPreferences(context, PREF_NAME)
  await prefs.put(TOKEN_KEY, token)
  await prefs.put(REFRESH_TOKEN_KEY, refreshToken)
  await prefs.flush()
}

// 从持久化中恢复 token(App 启动时调用)
export async function restoreToken(context: common.UIAbilityContext) {
  try {
    const prefs = await preferences.getPreferences(context, PREF_NAME)
    _token = (await prefs.get(TOKEN_KEY, '')) as string
    _refreshToken = (await prefs.get(REFRESH_TOKEN_KEY, '')) as string
  } catch {
    _token = ''
    _refreshToken = ''
  }
}

// 清除 token(退出登录时调用)
export async function clearToken(context?: common.UIAbilityContext) {
  _token = ''
  _refreshToken = ''
  if (context) {
    const prefs = await preferences.getPreferences(context, PREF_NAME)
    await prefs.delete(TOKEN_KEY)
    await prefs.delete(REFRESH_TOKEN_KEY)
    await prefs.flush()
  }
}

// 使用 refreshToken 换新的 access token
export async function refreshTokenRequest(): Promise<string> {
  if (!_refreshToken) {
    throw new Error('无 refreshToken,需要重新登录')
  }
  // 注意:这里直接用 axios 原始实例,避免循环拦截
  const res = await axios.post('https://api.example.com/auth/refresh', {
    refreshToken: _refreshToken
  })
  const newToken: string = res.data.data.accessToken
  _token = newToken
  return newToken
}

第四步:封装生产级请求层

有了 auth 模块,现在来封装核心请求层。新建 utils/request.ets

typescript 复制代码
// utils/request.ets
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from '@ohos/axios'
import { getToken, refreshTokenRequest, clearToken } from './auth'
import router from '@ohos.router'

// --------- 通用响应结构 ---------
export interface ApiResponse<T = unknown> {
  code: number
  message: string
  data: T
}

// --------- 创建实例 ---------
const request: AxiosInstance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 15000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// --------- 请求拦截器:自动注入 Token ---------
request.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = getToken()
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// --------- 响应拦截器:统一处理 + Token 刷新 ---------
let isRefreshing = false
let pendingQueue: Array<(token: string) => void> = []

request.interceptors.response.use(
  (response: AxiosResponse<ApiResponse<unknown>>) => {
    const { code, message } = response.data
    // 业务层错误(HTTP 200 但 code 不对)
    if (code !== 0 && code !== 200) {
      console.error(`业务错误 [${code}]: ${message}`)
      return Promise.reject(new Error(message))
    }
    // 返回完整的 data 对象,业务层自己取 .data
    return response
  },
  async (error) => {
    const { response, config } = error

    if (response?.status === 401) {
      if (!isRefreshing) {
        isRefreshing = true
        try {
          const newToken = await refreshTokenRequest()
          pendingQueue.forEach(resolve => resolve(newToken))
          pendingQueue = []
          config.headers['Authorization'] = `Bearer ${newToken}`
          return request(config)
        } catch (refreshError) {
          pendingQueue = []
          clearToken()
          router.replaceUrl({ url: 'pages/Login' })
          return Promise.reject(refreshError)
        } finally {
          isRefreshing = false
        }
      } else {
        // 已在刷新中,当前请求进队列等待
        return new Promise((resolve) => {
          pendingQueue.push((newToken: string) => {
            config.headers['Authorization'] = `Bearer ${newToken}`
            resolve(request(config))
          })
        })
      }
    }

    // 其他 HTTP 错误
    const errorMessages: Record<number, string> = {
      400: '请求参数有误',
      403: '没有访问权限',
      404: '接口不存在',
      500: '服务器内部错误',
      503: '服务暂时不可用'
    }
    const msg = errorMessages[response?.status] ?? `网络错误 (${response?.status ?? '未知'})`
    console.error(msg)
    return Promise.reject(new Error(msg))
  }
)

// --------- 对外暴露的便捷方法 ---------
export function get<T>(url: string, params?: Record<string, unknown>): Promise<ApiResponse<T>> {
  return request.get<ApiResponse<T>>(url, { params })
    .then(res => res.data as ApiResponse<T>)
}

export function post<T>(url: string, data?: Record<string, unknown>): Promise<ApiResponse<T>> {
  return request.post<ApiResponse<T>>(url, data)
    .then(res => res.data as ApiResponse<T>)
}

export function upload<T>(url: string, formData: FormData): Promise<ApiResponse<T>> {
  return request.post<ApiResponse<T>>(url, formData, {
    headers: { 'Content-Type': 'multipart/form-data' }
  }).then(res => res.data as ApiResponse<T>)
}

export { request }

第五步:请求取消------翻页/切 Tab 时防止接口乱序

列表页快速翻页时,旧请求的响应可能比新请求晚到,导致数据乱序。用 AbortController 取消过期请求:

typescript 复制代码
// pages/NewsList.ets
import { get } from '../utils/request'

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

@Entry
@Component
struct NewsList {
  @State list: NewsItem[] = []
  @State page: number = 1

  // 每次翻页重新创建,持有当前请求的取消控制器
  private abortController: AbortController | null = null

  async loadPage(pageNum: number) {
    // 取消上一次还未完成的请求
    this.abortController?.abort()
    this.abortController = new AbortController()

    try {
      const res = await get<NewsItem[]>('/news', {
        page: pageNum,
        size: 20,
        signal: this.abortController.signal  // 传入 signal
      })
      this.list = res.data
      this.page = pageNum
    } catch (err) {
      // AbortError 是正常取消,不用弹错误提示
      if ((err as Error).name !== 'AbortError') {
        console.error('加载失败:', (err as Error).message)
      }
    }
  }

  build() {
    Column() {
      List() {
        ForEach(this.list, (item: NewsItem) => {
          ListItem() {
            Text(item.title)
              .fontSize(16)
              .padding(12)
          }
        })
      }

      Row() {
        Button('上一页')
          .onClick(() => this.loadPage(this.page - 1))
          .enabled(this.page > 1)
        Text(`第 ${this.page} 页`)
          .margin({ left: 16, right: 16 })
        Button('下一页')
          .onClick(() => this.loadPage(this.page + 1))
      }
      .padding(16)
    }
  }

  aboutToAppear() {
    this.loadPage(1)
  }

  // 组件销毁时也要取消未完成的请求
  aboutToDisappear() {
    this.abortController?.abort()
  }
}

第六步:在业务层中调用

有了封装层,定义接口和页面调用都变得很干净:

typescript 复制代码
// api/user.ets
import { get, post } from '../utils/request'

export interface UserInfo {
  id: number
  name: string
  avatar: string
}

export function getUserInfo(userId: number) {
  return get<UserInfo>(`/user/${userId}`)
}

export function login(phone: string, password: string) {
  return post<{ token: string; refreshToken: string; userInfo: UserInfo }>(
    '/auth/login',
    { phone, password }
  )
}
typescript 复制代码
// pages/Profile.ets
import { getUserInfo, UserInfo } from '../api/user'

@Entry
@Component
struct Profile {
  @State userInfo: UserInfo | null = null
  @State loading: boolean = false
  @State errorMsg: string = ''

  async aboutToAppear() {
    this.loading = true
    try {
      const res = await getUserInfo(123)
      this.userInfo = res.data
    } catch (err) {
      this.errorMsg = (err as Error).message ?? '加载失败'
    } finally {
      this.loading = false
    }
  }

  build() {
    Column() {
      if (this.loading) {
        LoadingProgress().width(40).height(40)
      } else if (this.errorMsg) {
        Text(this.errorMsg).fontColor('#FF4444')
      } else if (this.userInfo) {
        Text(`你好,${this.userInfo.name}`).fontSize(20)
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

常见问题 Q&A

Q1:接口在浏览器能访问,在模拟器上请求失败?

检查两点:一是 module.json5 的 INTERNET 权限有没有加;二是接口是否是 HTTP 明文。鸿蒙默认不允许 HTTP 明文请求,需要在 module.json5 里配置 networkSecurityConfig 允许明文,或者后端直接升到 HTTPS。

Q2:并发多个请求时,Token 刷新逻辑会重复触发?

上文用 isRefreshing 标记 + pendingQueue 队列解决了这个问题。第一个 401 触发刷新,后续 401 全部进队列。刷新完成后统一用新 Token 重试,整个过程只发一次刷新请求。

Q3:大文件下载怎么处理?

大文件(几 MB 以上)推荐用原生 http.downloadFile,支持进度回调:

typescript 复制代码
import http from '@ohos.net.http'

const httpRequest = http.createHttp()
httpRequest.on('dataReceiveProgress', (data) => {
  const percent = Math.floor((data.receiveSize / data.totalSize) * 100)
  console.log(`下载进度: ${percent}%`)
})

await httpRequest.requestInStream(
  'https://example.com/bigfile.zip',
  { method: http.RequestMethod.GET }
)
httpRequest.destroy()

小文件(图片、配置)用 axios 的 responseType: 'arraybuffer' 即可。


总结

整个网络层的搭建涉及四个文件,各司其职:

文件 职责
utils/auth.ets Token 存取 + 刷新
utils/request.ets axios 实例 + 拦截器 + 封装方法
api/*.ets 按业务模块定义具体接口
pages/*.ets 只管调用 api,不关心网络细节

几个核心要点:

  1. 原生 http 简单好用,但别忘了 destroy()
  2. 生产项目用 axios,拦截器处理通用逻辑
  3. Token 刷新一定要加队列机制,防止并发重复刷
  4. 快速翻页等场景记得取消过期请求
  5. auth 工具要用持久化存储,App 重启后 Token 不丢

网络层做好了,后续所有业务接口的开发都会轻松很多。如果还没用上这套封装,拿走就行,稍微改下 baseURL 和接口响应结构就能直接用。


有问题欢迎评论区交流,点个赞再走~

相关推荐
24zhgjx-lxq3 小时前
OSPF的网络类型:P2P与Broadcast
网络·智能路由器·p2p·broadcast·ensp
算法即正义4 小时前
知识竞赛系统网络架构设计:高并发、高可用与安全实践
网络·安全
vortex54 小时前
【红队】企业内部安全区域划分与攻防思路解析
网络·安全·网络安全·渗透测试
墨染倾城殇4 小时前
FSC-BW5028MV适配车载多场景方案:WiFi7+蓝牙5.4 让音频与数据并发稳定输出
网络·音视频·wifi 7·蓝牙5.4·车载蓝牙模块
952364 小时前
网络原理TCP/UDP
网络
@insist1234 小时前
网络工程师-因特网与网络互联(五):应用层协议与互联网新技术
网络·网络工程师·软考·软件水平考试
世人万千丶4 小时前
Flutter 框架跨平台鸿蒙开发 - 时间胶囊慢递应用
学习·flutter·华为·开源·harmonyos·鸿蒙
世人万千丶1 天前
Flutter 框架跨平台鸿蒙开发 - 恐惧清单应用
学习·flutter·华为·开源·harmonyos·鸿蒙
行乾1 天前
鸿蒙端 IMSDK 架构探索
架构·harmonyos