
做鸿蒙原生应用有一阵子了项目里服务端用的 Spring Boot最开始大家图省事所有网络请求都直接拿ohos.net.http在页面里裸写。结果接口数量一多问题全来了Token 散落在各个调用点、超时设置靠心情、报错提示五花八门最恶心的是一到真机联调就冒出各种 502、上传失败、连不上本地服务的问题。后来我把整个网络请求层收敛成一套基于 Axios 的封装才算真正稳下来。这篇文章就把我踩过的坑、封装思路、以及生产环境必须考虑的细节全部展开讲一遍适合正在用 ArkTS 写鸿蒙应用、想把网络请求层整理成规范的开发者参考。1. 为什么我不建议在业务代码里裸写ohos.net.http1.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.httpohos/axios基于原生自己封一层拦截器无有请求/响应双向拦截需要自己设计统一错误分类每个调用点各自处理响应拦截器集中处理可以集中但代码量大Token 自动续期自己写状态管理可在拦截器内做队列重放自己写状态管理取消请求支持 destroy颗粒度较粗支持 CancelToken/AbortSignal需要自己封装上传/下载进度需要自己处理流式回调有现成进度事件需要自己处理连接复用取决于实例复用方式底层持有 HttpClient天然复用取决于你的封装质量社区资料官方文档较分散Web 生态资料多迁移成本低完全自己维护依赖体积无额外依赖一个 ohpm 包无额外依赖结论其实很清晰如果只是偶尔调两三个接口原生 HTTP 完全够用没必要引依赖。但如果你要打造一个生产级请求层ohos/axios是当前性价比最高的选择。它帮你把 Web 生态里验证过的最佳实践直接搬到了 ArkTS 里省掉大量造轮子的时间。2.3 安装与环境配置里容易踩的三个坑选型定了安装配置这块有几个坑真的得单独说。第一个坑别用 Web 版 axios 的包名去装。有些同学习惯性执行ohpm install axios装进来的是纯 Web 实现底层依赖浏览器的XMLHttpRequest或fetch在鸿蒙运行时根本跑不通。正确姿势是在 OpenHarmony 仓库里安装ohos/axiosohpm install ohos/axios装完之后查看oh-package.json5确认依赖名是ohos/axios不是axios。第二个坑忘了在 module.json5 里声明网络权限。ArkTS 应用默认是没有网络访问权限的不声明的话任何请求发出去都会在系统层被拒表现是请求直接异常错误信息还很迷惑一度让我以为是域名写错了。在module.json5的module节点里加上{ 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.tscore/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 ApiResponseT 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、methodToken 失效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-Length、Transfer-Encoding、Host等 Header 非常敏感客户端如果自己拼了 Header很容易触发网关异常。用ohos/axios的话尽量别手动改这些底层字段交给库去维护。4.2 真机调试连不上本机服务的真正原因这是新手绕不过去的一个坑在模拟器里请求http://127.0.0.1:8080是通的一上真机就疯狂超时或者报连接失败。很多人第一反应是代码有问题其实原因非常简单模拟器里的127.0.0.1指的是模拟器自己而真机上的127.0.0.1指的是手机自己两者都不指向你的电脑。真机要访问电脑上的服务有几个可行的办法用局域网 IP。电脑和手机连同一个 Wi-Fi请求地址写成http://192.168.x.x:8080。Windows 用ipconfigmacOS 用ifconfig查 IP。注意防火墙要允许该端口入站。使用adb reverse映射端口。真机通过 USB 连接时执行hdc reverse tcp:8080 tcp:8080HarmonyOS 用 hdc之后真机请求http://127.0.0.1:8080会转发到电脑本地的 8080。把服务部署到内网测试机或云端。适合后端服务依赖环境较多的场景。这里还要特别提醒一句联调阶段用明文 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): Promiseunknown { 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 requestT( config: AxiosRequestConfig, options?: { retryCount?: number; retryDelay?: number } ): PromiseT { 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 limitConcurrencyT(task: () PromiseT): PromiseT { return new PromiseT((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 管理这四块先立起来后面的业务开发都会非常顺畅。希望这份实战经验能让你少走几步弯路如果你们项目里有更好的方案也欢迎交流互补。