鸿蒙原生应用网络请求层封装实践:Axios拦截器、Token续期与联调避坑
2026/9/16 3:45:00 网站建设 项目流程

做鸿蒙原生应用有一阵子了,项目里服务端用的 Spring Boot,最开始大家图省事,所有网络请求都直接拿@ohos.net.http在页面里裸写。结果接口数量一多,问题全来了:Token 散落在各个调用点、超时设置靠心情、报错提示五花八门,最恶心的是一到真机联调就冒出各种 502、上传失败、连不上本地服务的问题。后来我把整个网络请求层收敛成一套基于 Axios 的封装,才算真正稳下来。这篇文章就把我踩过的坑、封装思路、以及生产环境必须考虑的细节全部展开讲一遍,适合正在用 ArkTS 写鸿蒙应用、想把网络请求层整理成规范的开发者参考。

1. 为什么我不建议在业务代码里裸写@ohos.net.http

1.1 原生 HTTP 请求到底能做什么:一次基础调用回顾

@ohos.net.http是鸿蒙系统提供的原生 HTTP 能力,最基础的用法是创建一个 HttpClient 实例,然后发起请求。一个简单的 GET 调用长这样:

import { http } from '@kit.NetworkKit'; import { BusinessError } from '@kit.BasicServicesKit'; const httpRequest = http.createHttp(); httpRequest.request( 'https://api.example.com/v1/products', { method: http.RequestMethod.GET, header: { 'Content-Type': 'application/json' }, connectTimeout: 10000, readTimeout: 10000, expectDataType: http.HttpDataType.STRING, } ).then((response: http.HttpResponse) => { // response.result 可能是 string,也可能是 ArrayBuffer const result = JSON.parse(response.result as string); console.info(`code=${response.responseCode}, data=${JSON.stringify(result)}`); }).catch((error: BusinessError) => { console.error(`request failed: ${JSON.stringify(error)}`); });

这个 API 本身不复杂,request一次是 Promise 风格,也支持回调。基础能力是够的:能设置 method、header、超时、期望返回类型、是否使用缓存,甚至还支持优先级。如果你只是临时调试一个接口,用它完全没问题,代码量也很小。

但问题就出在"临时调试"这四个字上。一旦你的业务开始认真起来,接口数量超过十个、页面超过五个,原生写法会迅速变得难以维护。这倒不是原生 HTTP 本身能力不行,而是它太"底层"了,把太多本该由请求层统一处理的事情摊还给了每个调用者。

1.2 项目一变大,裸写的四个崩点

我后来复盘了一下,裸写@ohos.net.http在项目变大后必然会崩掉的点,主要集中在四个地方。

第一个是Token 注入和续期没法统一处理。接口里需要带上登录态,最常见的做法是每个页面在请求前手动从 Storage 里取 Token,然后塞进 header。Token 过期了怎么办?后端返回 401,每个页面的 catch 里各写各的提示逻辑,有的页面弹窗,有的页面跳登录,有的页面直接静默失败。你根本没办法在一个地方统一感知"哦,用户登录态失效了,我该刷新 Token 或者统一踢出登录"。

第二个是超时和错误处理的标准不统一。有人给connectTimeout写成 5 秒,有人写成 30 秒,还有人干脆不写。后端业务码和 HTTP 状态码混在一起,前端拿到 response 之后每个人各解析各的,A 页面判断code === 0算成功,B 页面判断code === 200才成功。这种写法看着是小问题,出事故的时候排查成本翻倍。

第三个是没有拦截器机制,导致横切逻辑无处安放。比如每次请求想往 header 里加一个 requestId 方便排查、想统计接口耗时、想统一打印请求日志,甚至想做请求去重,这些逻辑在裸写模式下只能靠复制粘贴。一旦需求变化,你得把所有调用点翻出来改一遍,改漏一个就是线上 bug。

第四个是连接复用和性能优化无从下手。鸿蒙底层的 HTTP 能力本身是有连接复用机制的,但如果你在每个页面都createHttp()创建一个新实例,连接池的收益会被稀释;端口、DNS、TLS 握手的开销也会累积。生产环境里网络慢,很多时候不是带宽问题,而是握手次数太多、连接没有复用。

1.3 最重要的一个认知:请求层要独立出来

经历了这次重构,我最大的体会其实是认知层面的:无论用原生 HTTP 还是 Axios,请求层都应该是独立的,不应该散落在业务代码里。所谓独立请求层,就是把 baseURL、超时、Header 注入、Token 刷新、错误分类、日志上报这些横切关注点收敛到一个模块里,页面只管调用ProductApi.getList()UserApi.login()这样的方法,拿到干净的 Promise 结果。

有了这个意识,你再来看接下来要讲的那套 Axios 封装,就会明白它的价值不是"多引了一个依赖",而是让你拥有了一个可以集中处理横切逻辑的枢纽层。

2.@ohos/axios选型分析:不是换了个库,是换了一套开发方式

2.1 为什么选社区移植的@ohos/axios

鸿蒙生态里做网络请求,@ohos/axios是一个绕不开的开源库。它是把 Web 生态里非常成熟的 Axios 移植到鸿蒙上的产物,API 设计基本对齐 Web 版,核心的拦截器、实例、取消请求、上传下载这些能力都保留下来了。当初选型的时候,我还真对比过三条路:继续用原生 HTTP 包一层、用@ohos/axios、或者自己从零写一个请求工具。

最后选了@ohos/axios,核心原因有三个。

第一,拦截器机制太实用了。请求发出前统一注入 Token、拼接签名、加 requestId;响应回来后先统一解析 HTTP 状态码、业务码,把"是否需要刷新 Token"这种逻辑收敛在同一个地方。这套开发方式在 Web 生态里已经被验证了很多年,直接搬过来用,心智成本极低。

第二,它底层依然走的是鸿蒙系统网络栈,不是自己另起炉灶实现一套 TCP 协议栈。这意味着它能享受到系统级的连接复用、网络状态感知这些能力,性能和稳定性有保障。

第三,类型提示友好。ArkTS 对类型要求比较严格,@ohos/axios带了完整的.d.ts类型定义,响应体、错误对象都能有类型推断。配合项目里定义的接口返回模型,写起来比裸调原生 HTTP 舒服太多。

2.2 与原生 HTTP、自封装方案的横向对比

为了把选型的理由说透,我整理了一个简单的对比表。这三个方案没有绝对优劣,关键看你的项目阶段和团队维护能力。

维度原生 @ohos.net.http@ohos/axios基于原生自己封一层
拦截器有,请求/响应双向拦截需要自己设计
统一错误分类每个调用点各自处理响应拦截器集中处理可以集中,但代码量大
Token 自动续期自己写状态管理可在拦截器内做队列重放自己写状态管理
取消请求支持 destroy,颗粒度较粗支持 CancelToken/AbortSignal需要自己封装
上传/下载进度需要自己处理流式回调有现成进度事件需要自己处理
连接复用取决于实例复用方式底层持有 HttpClient,天然复用取决于你的封装质量
社区资料官方文档较分散Web 生态资料多,迁移成本低完全自己维护
依赖体积无额外依赖一个 ohpm 包无额外依赖

结论其实很清晰:如果只是偶尔调两三个接口,原生 HTTP 完全够用,没必要引依赖。但如果你要打造一个"生产级请求层",@ohos/axios是当前性价比最高的选择。它帮你把 Web 生态里验证过的最佳实践直接搬到了 ArkTS 里,省掉大量造轮子的时间。

2.3 安装与环境配置里容易踩的三个坑

选型定了,安装配置这块有几个坑真的得单独说。

第一个坑,别用 Web 版 axios 的包名去装。有些同学习惯性执行ohpm install axios,装进来的是纯 Web 实现,底层依赖浏览器的XMLHttpRequestfetch,在鸿蒙运行时根本跑不通。正确姿势是在 OpenHarmony 仓库里安装@ohos/axios

ohpm install @ohos/axios

装完之后查看oh-package.json5,确认依赖名是@ohos/axios,不是axios

第二个坑,忘了在 module.json5 里声明网络权限。ArkTS 应用默认是没有网络访问权限的,不声明的话,任何请求发出去都会在系统层被拒,表现是请求直接异常,错误信息还很迷惑,一度让我以为是域名写错了。在module.json5module节点里加上:

{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }

第三个坑,API 版本和 SDK 版本匹配问题@ohos/axios对不同 HarmonyOS API 版本有不同要求,如果你项目的compileSdkVersion太低,可能会出现类型不兼容或者运行期崩溃。建议项目创建的时候用当前的稳定 SDK,装依赖前留意一下库的README里标注的最低 API Version。

这里多说一句,很多人在模拟器上跑通了,一上真机就翻车,这个我在第 4 节会展开讲。真机网络环境和模拟器差很多,尤其是访问本机服务、明文 HTTP、证书这一整块。

3. 请求层封装:实例、拦截器、统一响应的落地细节

3.1 目录与分层:让请求层像后端 service 一样清晰

封装之前先把目录设计好。我从后端那套 service 分层里借鉴了思路,前端请求层分成三层,职责非常清晰:

src/ ├── api/ │ ├── modules/ │ │ ├── product.api.ts │ │ └── user.api.ts │ └── types/ │ ├── product.d.ts │ └── user.d.ts ├── core/ │ ├── http/ │ │ ├── client.ts // axios 实例 + 拦截器 │ │ ├── error-code.ts // 错误码映射 │ │ └── service.ts // 泛型请求方法,组合 Token 刷新、重试等 │ ├── token-manager.ts │ └── logger.ts └── common/ └── app-config.ts
  • core/http是核心请求层,只关心网络,不关心业务。
  • api/modules是业务接口层,一个模块一个文件,方法里只做"传参数、拼接 URL、声明返回类型"。
  • api/types放接口出入参类型。

这样做的好处是,以后后端接口路径变了,你只需要改对应api/modules里的一个方法;请求策略变了,只改core/http;页面里永远只调ProductApi.getList()。职责单一,排查问题的时候能快速定位到层。

3.2 创建实例:baseURL、超时与默认 Header

请求层的核心入口是创建单例 axios 实例。注意两个词:单例实例

很多人会把axios.create放在每个调用的方法里,这就犯了我在第 1 节说的"实例不复用"的毛病。生产级请求层应该保证整个应用只有一个 client 实例,连接复用、拦截器注册都建立在这一个实例上。

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from '@ohos/axios'; import { AppConfig } from '../../common/app-config'; class HttpClient { private static instance: HttpClient; readonly client: AxiosInstance; private constructor() { this.client = axios.create({ baseURL: AppConfig.baseURL, timeout: 15000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, }); } public static getInstance(): HttpClient { if (!HttpClient.instance) { HttpClient.instance = new HttpClient(); } return HttpClient.instance; } } export const httpClient = HttpClient.getInstance();

timeout我建议设置成 10 到 15 秒,而不是拍脑袋设个 3 秒。移动端弱网环境下,3 秒很容易误伤正常请求;设太长了,用户长时间等待体验又差。15 秒是一个比较平衡的值,具体可以看业务场景,比如上传接口单独放宽到 30 秒以上。

baseURL不要写在代码里,我习惯放在app-config.ts里,按环境区分:

export const AppConfig = { baseURL: 'https://api.example.com/v1', // baseURL: 'http://192.168.1.100:8080/v1', // 联调环境 };

这里有个细节值得注意:联调环境和生产环境的 baseURL 不同,如果你用明文 HTTP 联调,还涉及网络安全性配置,我后面会专门讲。

3.3 请求拦截器里做了哪些事

请求拦截器是这套封装里最值得花心思的地方。我每次请求前做三件事:注入 Token、附加公共参数、打印请求日志。

注入 Token 很简单,从统一的TokenManager里拿,而不是各个页面自己取:

this.client.interceptors.request.use((config: AxiosRequestConfig) => { const token = TokenManager.getInstance().getAccessToken(); if (token) { config.headers = { ...config.headers, 'Authorization': `Bearer ${token}`, }; } // 公共参数,比如版本号、设备标识 config.headers['X-App-Version'] = AppConfig.version; config.headers['X-Device-Id'] = DeviceUtil.getDeviceId(); config.headers['X-Request-Id'] = createRequestId(); Logger.info(`[请求] ${config.method?.toUpperCase()} ${config.url}`, config.params || {}); return config; }, (error: Error) => { return Promise.reject(error); });

注入 Token 这个动作,看起来是省了每个页面的重复代码,但真正重要的一点是:它为后续的"401 自动续期"提供了唯一的入口。因为所有请求都在这里带上了 Token,所以你才能在里面判断"Token 是否即将过期""是否需要提前刷新"。如果不经过拦截器,你根本不知道请求什么时候发出去的。

公共参数别加太多,加一个X-Request-Id(每次请求生成的 UUID)对排查线上问题特别有用。后端日志一旦记下你传过去的 requestId,打电话对账的时候,两边把日志一拼,整个调用链就出来了。

3.4 响应拦截器:错误分类与业务码处理

响应拦截器是另一个核心。它要做的事是:把"网络错误""HTTP 错误""业务错误""请求被取消"这四类完全不同的失败形式,收敛成统一的错误对象抛给页面,同时把成功响应里的业务数据data直接解出来。

一个可落地的写法是这样:

export interface ApiResponse<T = unknown> { code: number; message: string; data: T; } export class RequestError extends Error { code: number; httpStatus?: number; constructor(code: number, message: string, httpStatus?: number) { super(message); this.code = code; this.httpStatus = httpStatus; } } this.client.interceptors.response.use( (response: AxiosResponse) => { const body = response.data as ApiResponse; Logger.info(`[响应] ${response.config.url} status=${response.status} code=${body?.code}`); if (response.status === 200) { if (body && body.code === 0) { return body.data; // 业务成功,直接返回 data } // 业务码非 0:业务失败 if (body && body.code === 401) { return handleUnAuthorized(response.config); // 触发统一续期逻辑 } return Promise.reject(new RequestError(body.code, body.message)); } return Promise.reject(new RequestError(-1, `HTTP ${response.status}`, response.status)); }, (error: Error) => { return Promise.reject(classifyNetworkError(error)); } ); function classifyNetworkError(error: Error): RequestError { // 超时、断网、DNS 失败归为网络错误 // 这里可根据 error.code 或 error.message 判断 const message = error.message || ''; if (message.includes('timeout') || message.includes('Network is unreachable')) { return new RequestError(-2, '网络不给力,请检查网络连接'); } if (message.includes('cancel')) { return new RequestError(-3, '请求已取消'); } return new RequestError(-1, '网络异常,请稍后重试'); }

这里有一个很多人容易搞混的点:HTTP 200 不等于业务成功,业务成功也不代表 HTTP 一定是 200。很多后端在业务失败时返回 HTTP 200 但code非 0,也有极端情况是 HTTP 200 但 body 结构不完整。所以响应拦截器里必须先判断 HTTP 状态,再判断业务码,两层分开处理。

封装完的效果是,页面里拿到 Promise 只有两种情况:要么是干净的data数据,要么是一个语义清晰的RequestError错误对象。页面里再也不用写res.code === 0这种判断了。

3.5 错误文案映射表:用户看到的不该是"网络错误"

错误分类做好之后,还有个体验层面的细节:不能把所有错误都返给用户一句"网络错误"。我把常见错误码整理成了映射表,在 UI 层统一展示:

错误场景错误码用户提示开发日志
网络不可用-2当前网络不可用,请检查网络设置完整错误堆栈 + 请求 URL
请求超时-2网络有点慢,请稍后重试耗时、URL、method
Token 失效401登录已过期,请重新登录触发刷新或跳登录
业务校验失败4xxxxmessage 原样展示冗余字段打点
服务器异常5xx服务器开小差了,请稍后重试HTTP 状态码 + body

这张表不是静态写死的,实际项目里我建议把它做成可配置。后端如果调整了错误码,前端只需要改配置文件,不需要改业务代码。这也是请求层独立性的一个体现。

4. 联调阶段的高频事故:502、真机连不上、上传失败

4.1 一个 502 的完整排查链路

联调阶段最常见的报错之一就是:unexpected status 502 bad gateway。我见过很多同学一看到 502 就慌了,以为是自己代码问题,其实 502 的语义很明确:你请求的网关或代理服务没有拿到上游服务的合法响应。也就是说,请求确实发出去了,但中间某个环节断了。

我自己的排查链路是这样的,分享出来给大家参考。

第一步,确认 502 是从哪一层出来的。如果你的请求地址是http://127.0.0.1:1572这样的本地代理,大概率是本机开发代理或本地网关进程挂了,或者是代理转发到上游时超时。去后台看一眼目标服务进程是不是还活着,本地代理进程是不是已经退出,很多时候重启一下就能解决。

第二步,用 curl 复现一下请求。我经常在排查这类问题的时候先绕开客户端,在电脑上直接 curl 同样的 URL、同样的 body,看能不能得到正常响应。如果 curl 也 502,问题基本可以锁定在后端或代理层;如果 curl 正常但 App 里 502,再回头看客户端的 URL、Header、超时配置有没有问题。

第三步,看代理层和上游服务的日志。502 背后常见的两个根因是:上游服务处理时间过长超过了网关超时时间,或者请求体/响应体太大超出网关缓冲区限制。前者把超时时间调大,后者调大proxy_buffer_size或者检查上传文件大小。

第四步,检查请求头是否有奇怪的字段。有些网关对Content-LengthTransfer-EncodingHost等 Header 非常敏感,客户端如果自己拼了 Header,很容易触发网关异常。用@ohos/axios的话,尽量别手动改这些底层字段,交给库去维护。

4.2 真机调试连不上本机服务的真正原因

这是新手绕不过去的一个坑:在模拟器里请求http://127.0.0.1:8080是通的,一上真机就疯狂超时或者报连接失败。很多人第一反应是代码有问题,其实原因非常简单:模拟器里的127.0.0.1指的是模拟器自己,而真机上的127.0.0.1指的是手机自己,两者都不指向你的电脑

真机要访问电脑上的服务,有几个可行的办法:

  1. 用局域网 IP。电脑和手机连同一个 Wi-Fi,请求地址写成http://192.168.x.x:8080。Windows 用ipconfig,macOS 用ifconfig查 IP。注意防火墙要允许该端口入站。
  2. 使用adb reverse映射端口。真机通过 USB 连接时,执行hdc reverse tcp:8080 tcp:8080(HarmonyOS 用 hdc),之后真机请求http://127.0.0.1:8080会转发到电脑本地的 8080。
  3. 把服务部署到内网测试机或云端。适合后端服务依赖环境较多的场景。

这里还要特别提醒一句:联调阶段用明文 HTTP 是可以接受的,但上线务必切 HTTPS。另外,不要为了图省事把测试环境的 baseURL 顺手留在生产包里,这种低级事故我见过不止一次。

4.3 文件上传失败的三种常见姿势

文件上传是网络请求里翻车率最高的场景之一,常见有三种姿势导致失败。

姿势一:把文件路径直接塞进 JSON 里当 base64 传。这种做法在小文件时勉强能跑,文件一大会撑爆内存,而且服务端往往不认这种格式。正确做法是用multipart/form-data上传。

@ohos/axios里上传文件,可以通过 FormData 构造:

import { FormData } from '@ohos/axios'; const formData = new FormData(); formData.append('file', { uri: fileUri, // 文件的 URI name: 'photo.jpg', type: 'image/jpeg', }); formData.append('bizType', 'avatar'); httpClient.client.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, // 上传场景超时单独放宽 timeout: 60000, });

姿势二:文件 URI 没有转换成真实可读路径。鸿蒙的沙箱权限比较严格,你从文件选择器拿到的uri不一定能直接被上传组件读取。通常需要先通过文件管理服务解析出真实路径,或者确保应用有对应文件目录的读写权限,否则上传组件会报"文件不存在"或者"上传失败:网络请求错误"。

姿势三:上传大文件没有做进度展示和超时控制。用户点击上传后干等几十秒没有任何反馈,然后突然失败,体验极差。@ohos/axios支持上传进度回调,应该把进度展示出来,同时把该接口的 timeout 调大,别让 15 秒的默认超时把大文件请求掐断。

progress 事件里注意:一定要在主线程更新 UI,不要直接在回调里操作组件,ArkTS 对线程调度要求比较严格。

4.4 HTTP 明文与证书校验

鸿蒙应用默认对明文 HTTP 是有限制的,这跟 iOS 的 ATS 类似。你在联调阶段请求http://192.168.1.100:8080,可能直接被拦截,报错信息还不太明显。解决办法是配置网络安全策略,允许特定域名的明文流量。

module.json5里注册网络安全配置:

{ "module": { "metadata": [ { "name": "network_security_config", "value": "resource/profile/network_config.json" } ] } }

network_config.json里可以配置基础策略、域名例外、证书相关设置。大致结构长这样:

{ "network-security-config": { "base-config": { "cleartext-traffic-permitted": true }, "domain-config": [ { "domains": ["192.168.1.100"], "cleartext-traffic-permitted": true } ] } }

注意这个配置千万别直接用cleartext-traffic-permitted: true全局放开然后忘掉,上线前一定要收紧,只保留必要的 HTTPS 请求。证书校验方面,生产环境强烈建议使用正规 CA 签发的证书,不要用自签名证书。如果非要自签名,也要把证书内置到应用里做固定校验,否则中间人攻击风险很高。

5. 进阶能力:Token 自动续期、重试、取消与连接复用

5.1 401 统一刷新并重放请求队列

当 Token 过期,后端返回 401,生产级请求层要做的是:自动刷新 Token,然后重放排队中的失败请求。页面里完全感知不到这个过程,用户仍然处于登录态。

实现思路是这样的:

let isRefreshing = false; let pendingQueue: Array<(token: string) => void> = []; async function handleUnAuthorized(config: AxiosRequestConfig): Promise<unknown> { if (isRefreshing) { // 已经有请求在刷新 Token,把当前请求挂到队列等待 return new Promise((resolve, reject) => { pendingQueue.push((newToken: string) => { config.headers = { ...config.headers, 'Authorization': `Bearer ${newToken}` }; resolve(httpClient.client.request(config)); }); }); } isRefreshing = true; try { const newToken = await TokenManager.getInstance().refreshToken(); // 重放队列里的所有请求 pendingQueue.forEach((callback) => callback(newToken)); pendingQueue = []; config.headers = { ...config.headers, 'Authorization': `Bearer ${newToken}` }; return await httpClient.client.request(config); } catch (error) { // 刷新失败,通常这里统一踢出登录 TokenManager.getInstance().clear(); throw new RequestError(401, '登录已过期,请重新登录'); } finally { isRefreshing = false; } }

这个方案的巧妙之处在于用一个isRefreshing标志保证同一时间只有一个刷新请求在飞,其余并发请求排队等待,刷新完成后统一重放。如果刷新接口本身失败了,注意别死循环,需要设置重试次数上限。

这里还有一个细节:刷新 Token 的请求自身不能走业务拦截器里的handleUnAuthorized逻辑,否则会递归调用自己。我习惯把它单独用裸 axios 实例发,或者给这个请求加一个标记,拦截器里识别到后直接放行。

5.2 什么请求可以自动重试,什么不能

自动重试是个好功能,但用不好会放大故障。原则很简单:只有幂等请求才适合自动重试。GET、HEAD、PUT、DELETE(严格说要看语义)通常可以安全重试;POST 这种会创建资源的请求,重试可能导致重复下单、重复提交,必须谨慎。

我给请求层加了一个重试机制,在service.ts的泛型方法里控制:

async function request<T>( config: AxiosRequestConfig, options?: { retryCount?: number; retryDelay?: number } ): Promise<T> { const retryCount = options?.retryCount ?? 0; const retryDelay = options?.retryDelay ?? 1000; let lastError: unknown; for (let attempt = 0; attempt <= retryCount; attempt++) { try { return await httpClient.client.request(config); } catch (error) { lastError = error; if (attempt < retryCount && shouldRetry(config, error)) { await sleep(retryDelay * (attempt + 1)); // 退避,防止雪崩 continue; } break; } } throw lastError; } function shouldRetry(config: AxiosRequestConfig, error: unknown): boolean { if (config.method && config.method.toLowerCase() !== 'get') { return false; // 非 GET 不自动重试 } // 网络错误或 5xx 才重试;业务错误不重试 return error instanceof RequestError && (error.code === -2 || (error.httpStatus ?? 0) >= 500); }

重试次数别设太多,我一般 GET 请求最多重试 2 次,每次退避间隔递增,避免服务端已经过载时客户端还持续打流量,把故障放大。

5.3 页面销毁时取消请求

ArkTS 页面销毁以后,如果网络请求的回调才回来,直接去操作页面状态很容易报错,甚至造成内存泄漏。处理方式很简单:离开页面时取消还在进行的请求。

@ohos/axios支持CancelToken方式:

class PageApi { private cancelSource = axios.CancelToken.source(); async fetchList() { try { const data = await httpClient.client.get('/products', { cancelToken: this.cancelSource.token, }); // 成功的业务处理 } catch (error) { if (axios.isCancel(error)) { console.info('用户已离开页面,请求取消'); return; } // 其他错误处理 } } onPageHide() { this.cancelSource.cancel('page hidden'); } }

这里要注意,取消请求也会走 catch 分支,所以必须判断axios.isCancel(error),否则会被当成真实错误处理,弹出错误提示,体验就很怪。在封装的时候,我把取消错误单独归类成了"请求已取消",页面可以根据错误码跳过提示。

5.4 连接复用与并发控制

最后聊一个偏性能优化的点:连接复用。鸿蒙底层网络栈本身支持 HTTP 连接复用、Keep-Alive,但前提是你要让它复用起来。

实际开发里最容易破坏连接复用的操作就是"频繁创建新实例"。我在第 3 节强调过整个应用只保留一个 axios 单例,原因就在这里。每次axios.create()都会创建一个新的 HttpClient,等于把连接池打散了,握手开销自然上去了。你如果发现某段时间请求延迟突然高,先检查是不是有代码在循环里 create 实例。

并发控制也是生产环境要考虑的。像我们的应用,进入首页会同时发出七八个请求,如果服务端能力有限,瞬间高并发反而拖慢整体速度。我封装了一个简单的并发限制工具,思路是维护一个并发任务池,最大并发数可配:

const CONCURRENCY_LIMIT = 4; let activeCount = 0; const taskQueue: Array<() => void> = []; export async function limitConcurrency<T>(task: () => Promise<T>): Promise<T> { return new Promise<T>((resolve, reject) => { taskQueue.push(() => { activeCount++; task() .then(resolve) .catch(reject) .finally(() => { activeCount--; const next = taskQueue.shift(); if (next) next(); }); }); if (activeCount < CONCURRENCY_LIMIT) { const next = taskQueue.shift(); if (next) next(); } }); }

并发上限具体设多少,取决于后端服务的能力和接口耗时,4 到 6 是一个常见区间。这个功能不是每个项目都必须,但如果你的应用有大量并行首屏请求,值得加上。

6. 从这套封装到鸿蒙大赛项目:我的实践结果与建议

6.1 重构前后的直观对比

这套请求层封装完以后,我拿它重构了手头一个鸿蒙原生应用项目,效果是很直观的。项目里有 35 个左右的后端接口,重构前每个页面平均有 70 到 100 行跟网络请求相关的胶水代码:手动拼 URL、手动塞 Token、每种错误都写一遍 catch 分支。重构之后,每个业务接口方法平均只有 10 到 15 行,页面里几乎看不到任何跟 HTTP 细节相关的代码。

最明显的变化是排查问题的速度。以前线上反馈"某个接口报错",你得找到那个页面、复现、打日志。现在所有请求都有统一日志:URL、请求参数、响应状态、业务码、耗时全在一条日志里,配合 requestId,几分钟就能定位到是哪一步出的问题。

另一个直观变化是崩溃率和使用体验。超时统一处理、上传状态展示、Token 自动续期,这三个功能上线后,用户反馈的"请求失败"类问题明显减少。尤其是 Token 续期,之前用户停留久了 Token 过期,再操作就弹"登录过期请重新登录",现在直接静默续期,几乎无感知。

6.2 沉淀下来的结构可以直接抄

我把整个请求层最终沉淀下来的目录结构列出来,有需要的同学可以直接抄作业,再根据自己的接口规范调整:

core/http/ ├── client.ts // 单例 axios 实例,注册请求/响应拦截器 ├── service.ts // 泛型 request 方法,重试、并发控制在这里 ├── error-code.ts // 错误码映射表 ├── types.ts // 统一的 ApiResponse、PageResult 等通用类型 core/ ├── token-manager.ts // Token 存取、刷新逻辑 ├── logger.ts // 日志封装,区分 debug/release api/ ├── modules/ // 按业务域拆分的接口方法 └── types/ // 接口出入参类型

这套结构的关键设计原则就三条:核心请求层不依赖任何业务;业务接口层不写任何 HTTP 细节;页面只依赖业务接口层。只要遵守这三条,后续加接口、改接口、切换环境都很快。

6.3 新手最容易走的弯路

最后给刚开始接触鸿蒙网络请求的开发者几个建议。这些弯路我自己都走过,当时要是有人提前告诉我,能省不少时间。

第一,先跑通一个最简单的 GET 请求,再做封装。很多人一上来就想把请求层做得特别完善,结果环境都没配好,连基础请求都发不出去,封装代码自然无从调试。先把权限、基础请求、真机联调这三步跑顺,再考虑拦截器、Token 续期这些高级能力。

第二,不要为了封装而封装。如果项目只有五六个接口,其实不太需要完整的请求层,直接用@ohos.net.http写也没问题。封装是有成本的,只有当你发现重复代码开始变多、错误处理开始失控,才是真正需要引入 Axios 加统一封装的时机。请求层的目标是解决问题,不是展示技术。

第三,强烈建议在项目第一天就把日志和错误码规划好。这是我踩过最大的坑:项目早期没有统一日志,所有排错都靠对着代码猜;后来我把日志系统补上,排查效率提升了好几倍。错误码同理,后期再统一梳理,代价是巨大的。

第四,多留意@ohos/axios的版本更新和已知问题。这个库是社区维护的,API 在不同版本间可能有细微变化,尤其是取消请求、FormData 这些能力。我每次升库版本都会跑一遍上传、下载、取消、Token 续期这几个核心用例,避免升级引入回归。

网络请求层这种东西,写起来不算难,难的是把所有边界情况都考虑到、并且沉淀成一套稳定的规范。我个人现在接任何一个鸿蒙项目,第一天要做的第一件事就是把请求层搭好:单例 client、日志、错误码、Token 管理这四块先立起来,后面的业务开发都会非常顺畅。希望这份实战经验能让你少走几步弯路,如果你们项目里有更好的方案,也欢迎交流互补。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询