☰
【鸿蒙心迹】鸿蒙网络请求架构实战——@ohos.net.http 到 Axios 封装、拦截器与统一错误处理(HarmonyOS 7.x)
2026/9/30 8:32:06 网站建设 项目流程

摘要: 天气查询 App 上线前,我遇到一个诡异问题:真机上所有请求全部失败,Previewer 里却一切正常。排查到最后,根因既不是代码也不是网络,而是网络安全配置——HarmonyOS 对明文 HTTP 默认拦截。这个坑让我意识到,鸿蒙网络层的问题 80% 不在"怎么发请求",而在架构:权限、安全配置、统一封装、拦截器、错误处理。本文以天气 App 为贯穿场景,从 @ohos.net.http 原生 API 到 Axios 鸿蒙版封装,单点深挖拦截器与统一错误处理的设计,附超时/重试策略对比与 5 个真实踩坑。

适用版本: HarmonyOS NEXT 7.x / API 14+ / ohpm(2026 年稳定版)

开篇:权限配了,请求还是全部失败

“真机上所有请求都失败,但 Previewer 里好好的。”

2026 年 7 月底,天气查询 App 真机联调第一天。我信心满满地打包安装,打开 App 期待看到天气数据——结果屏幕上只有错误提示。回到 Previewer 里跑,接口正常返回。

这个"真机失败、预览器成功"的现象,我后来才知道是鸿蒙网络层的经典坑:默认网络安全配置只信任 HTTPS,明文 HTTP 请求被系统拦截。而 Previewer 走的是宿主环境的网络栈,不受这个限制。

真机: HTTP 请求 → 被网络安全配置拦截 Previewer: HTTP 请求 → 走宿主网络栈,正常

这一个坑让我排查了 2 小时。它暴露了一个更重要的问题:网络层的设计不是"怎么发请求",而是权限、安全、封装、错误处理这一整套架构。本文就按这个思路,把鸿蒙网络请求的完整架构讲透。

一、网络权限配置(第一道坎)

1.1 为什么请求全部失败

HarmonyOS 应用请求网络,必须先在 module.json5 声明权限,否则请求直接失败:

// entry/src/main/module.json5 { "module": { "name": "entry", "requestPermissions": [ { "name": "ohos.permission.INTERNET" // 网络访问权限 } ] } }

坑 1:漏配 INTERNET 权限,请求静默失败

现象: 请求代码看起来没问题,但 onFail 返回错误 2300006(无网络权限) 根因: 未在 module.json5 声明 ohos.permission.INTERNET

这里要说一下"静默失败"为什么如此高发:权限类错误不会在编译期暴露——INTERNET 权限属于 normal 级别,不弹窗、不提示,只在运行时请求发出去的那一刻以错误码 2300006 告知。而多数业务代码只处理了"成功给数据、失败给 Toast",把 onFail 里那个数字错误码原样吞掉或只打印了一行日志,用户侧看到的就是"什么都没发生"。这也是后面第 3.2 节要做统一错误映射的直接动机:把系统错误码在拦截器里翻译成人话,错误才不至于静默。

1.2 网络安全配置(HTTPS 与明文 HTTP)

HarmonyOS 默认只允许 HTTPS,明文 HTTP 会被拦截。开发期要访问本地/内网 HTTP 接口,必须配置网络安全:

// entry/src/main/resources/base/profile/network_config.json { "network-security-config": { "base-config": { "cleartext-traffic-permitted": true // 开发期允许明文 HTTP(上线必须改回 false) }, "domain-config": [ { "domains": [ { "name": "api.example.com", "include-subdomains": true } ], "cleartext-traffic-permitted": false } ] } }
// module.json5 中声明 networkConfig 引用 { "module": { "name": "entry", "metadata": [ { "name": "network_security_config", "resource": "$profile:network_config" } ] } }

安全红线: 明文 HTTP 只用于开发调试,上线前必须关闭。生产环境一律 HTTPS,否则存在中间人攻击风险。

二、@ohos.net.http 原生 API 实战

2.1 GET 请求(原生)

import{http}from'@kit.NetworkKit';asyncfunctiongetWeather(city:string):Promise<string>{consthttpRequest=http.createHttp();// 每个请求单独创建,用完销毁constresponse=awaithttpRequest.request(`https://api.example.com/weather?city=${encodeURIComponent(city)}`,{method:http.RequestMethod.GET,header:{'Content-Type':'application/json'},connectTimeout:10000,// 连接超时 10sreadTimeout:10000// 读取超时 10s});httpRequest.destroy();// 必须销毁,否则连接泄漏returnresponse.resultasstring;}

2.2 原生 API 的痛点

用原生 API 写业务代码,会遇到 4 个问题:

痛点表现
重复样板代码每个请求都要创建/销毁/解析
无统一错误处理每个请求自己 try-catch
无拦截器Token 注入、日志、重试都要手写
连接泄漏风险忘记 destroy() 导致连接耗尽

结论: 原生 API 适合单次请求、快速验证;业务项目必须封装。下面用 Axios(鸿蒙适配版)封装。

三、Axios 鸿蒙版封装实战(核心)

3.1 安装 Axios(鸿蒙适配版)

# 在工程根目录执行(ohpm 安装)ohpminstall@ohos/axios

3.2 封装统一 HTTP 客户端(拦截器 + 统一错误处理)

请求进来后的完整链路是:业务调用api.get→ 请求拦截器注入 token/公共头 → 发起请求 → 按结果分流:

  1. HTTP 状态码为 2xx → 响应拦截器剥离 data 层,返回业务数据;
  2. 401 → 刷新 token 后重放原请求;
  3. 5xx 且请求幂等 → 按退避策略重试,最多 2 次;
  4. 其他错误 → 统一错误映射,转成业务错误码后 Toast/错误页提示。
// common/http/client.etsimportaxios,{AxiosInstance,AxiosResponse,AxiosError}from'@ohos/axios';import{BusinessError}from'@kit.BasicServicesKit';// 统一响应结构exportinterfaceApiResponse<T>{code:number;message:string;data:T;}// 业务错误码exportclassApiErrorextendsError{code:number;constructor(code:number,message:string){super(message);this.code=code;}}classHttpClient{privateinstance:AxiosInstance;constructor(){this.instance=axios.create({baseURL:'https://api.example.com',timeout:15000,// 总超时 15sheaders:{'Content-Type':'application/json'}});// 请求拦截器:统一注入 Tokenthis.instance.interceptors.request.use((config)=>{consttoken=AppStorage.get<string>('token')??'';if(token){config.headers['Authorization']=`Bearer${token}`;}returnconfig;});// 响应拦截器:统一错误处理this.instance.interceptors.response.use((response:AxiosResponse)=>{constbody=response.dataasApiResponse<unknown>;// 业务码非 0 视为业务错误if(body.code!==0){returnPromise.reject(newApiError(body.code,body.message));}returnresponse;},(error:AxiosError)=>{// 网络层错误统一兜底returnPromise.reject(this.normalizeError(error));});}// 把各种错误归一化为友好信息privatenormalizeError(error:AxiosError):ApiError{if(error.code==='ECONNABORTED'){returnnewApiError(-1,'请求超时,请检查网络');}if(!error.response){returnnewApiError(-2,'网络连接失败,请检查网络');}conststatus=error.response.status;if(status===401){returnnewApiError(401,'登录已过期,请重新登录');}if(status===403){returnnewApiError(403,'没有权限访问');}if(status>=500){returnnewApiError(status,'服务器开小差了,请稍后再试');}returnnewApiError(status,`请求失败(${status})`);}// 对外统一 GETasyncget<T>(url:string,params?:Record<string,string>):Promise<T>{constresp=awaitthis.instance.get<ApiResponse<T>>(url,{params});return(resp.dataasApiResponse<T>).data;}// 对外统一 POSTasyncpost<T>(url:string,body:object):Promise<T>{constresp=awaitthis.instance.post<ApiResponse<T>>(url,body);return(resp.dataasApiResponse<T>).data;}}exportconsthttpClient=newHttpClient();

3.3 业务 API 封装(按模块拆分)

之所以再包一层 weatherApi,而不是让页面直接持有 httpClient 调 URL:URL、参数名、数据结构属于服务端契约,页面不应该感知。服务端改路径或字段时只改 api 层一处;同时天气查询是天气 App 的高频动作,api 层正好是挂接缓存与请求去重的位置。业务按模块拆 api 文件(weather/order/user),也让"这个接口谁在用"变得可检索。

// common/api/weather.etsimport{httpClient}from'../http/client';exportinterfaceWeatherInfo{city:string;temp:number;weather:string;humidity:number;}exportconstweatherApi={getCurrent:(city:string):Promise<WeatherInfo>=>httpClient.get<WeatherInfo>('/weather/current',{city}),getForecast:(city:string,days:number):Promise<WeatherInfo[]>=>httpClient.get<WeatherInfo[]>('/weather/forecast',{city,days:`${days}`})};

3.4 页面中调用(结合状态管理)

页面层只做三件事:管理 loading/errorMsg 两个 UI 状态、发起调用、展示结果。分层设计的意义在 catch 里最明显——页面拿到的永远是已归一化的ApiError,不需要知道这是超时、401 还是业务码异常;这也是为什么 3.2 的 normalizeError 必须做在拦截器而不是页面:错误语义在哪个层产生,就该在哪个层处理。

@Entry@Componentstruct WeatherPage{@Statecity:string='北京';@Stateweather:WeatherInfo|null=null;@Stateloading:boolean=false;@StateerrorMsg:string='';asyncloadWeather():Promise<void>{this.loading=true;this.errorMsg='';try{this.weather=awaitweatherApi.getCurrent(this.city);}catch(e){// 统一错误处理:页面只需展示 messageconsterr=easApiError;this.errorMsg=err.message;}finally{this.loading=false;}}build(){Column(){TextInput({placeholder:'输入城市',text:this.city}).onChange(v=>this.city=v)Button('查询天气').onClick(()=>this.loadWeather())if(this.loading){LoadingProgress()}elseif(this.errorMsg){Text(this.errorMsg)// 统一错误信息展示}elseif(this.weather){Text(`${this.weather.city}:${this.weather.temp}°C${this.weather.weather}`)}}}}

架构收益: 页面层永远只关心err.message,错误处理逻辑全部收敛在拦截器——新增错误码只需改一处。

四、超时、重试与并发控制

4.1 超时与重试策略

策略配置适用场景
连接超时connectTimeout: 10s网络切换、弱网
读取超时readTimeout: 10s服务端慢响应
总超时timeout: 15s兜底
自动重试失败重试 2 次(幂等 GET)网络抖动
退避策略500ms → 2s 指数退避避免重试风暴

4.2 重试实现(仅幂等请求)

asyncfunctiongetWithRetry<T>(fn:()=>Promise<T>,retries=2):Promise<T>{letlastError:Error|undefined;for(leti=0;i<=retries;i++){try{returnawaitfn();}catch(e){lastError=easError;// 指数退避:500ms、1sawaitsleep(500*Math.pow(2,i));}}throwlastError;}functionsleep(ms:number):Promise<void>{returnnewPromise(resolve=>setTimeout(resolve,ms));}// 使用:只对幂等的 GET 重试constdata=awaitgetWithRetry(()=>weatherApi.getCurrent('北京'));

退避参数怎么取:起始间隔 500ms是在"用户等待体感"和"给服务端喘息"之间取的折中——再短(如 100ms)第二次重试大概率撞上同一个故障窗口,纯属浪费;再长(如 2s 起步)用户盯着转圈的时间明显变差。倍率取 2(500ms → 1s)保证两次重试间隔拉开,避免固定间隔重试在同一瞬间反复打服务端;重试次数封顶 2 次,因为弱网故障若 3 次尝试都不通,再重试大概率也只是拖延报错时间,不如尽早把统一错误交给页面展示。

重试红线: 只对幂等请求(GET)重试。POST/支付/下单绝不自动重试,否则可能重复扣款/重复下单。

五、5 个真实踩坑与根因

1. 真机请求失败,Previewer 却能通

现象: Previewer 正常,真机全部请求失败 根因: 网络安全配置(明文 HTTP 拦截)只在真机生效;Previewer 走宿主网络栈 解法: 开发期配置 network_config.json 允许明文;上线关闭

2. 忘记 destroy(),连接数耗尽

现象: 连续请求 20+ 次后,后续请求全部超时 根因: http.createHttp() 创建未销毁,连接泄漏 解法: 原生 API 每个请求结束必须 httpRequest.destroy();或直接用 Axios 封装(内部管理)

3. 中文参数未编码,请求 400

现象: 查询"北京"返回 400,查询"beijing"正常 根因: URL 中文未 encodeURIComponent 解法: httpClient.get 的 params 内部自动编码(axios 默认),原生 API 需手动 encodeURIComponent

4. Token 过期只提示"请求失败"

现象: 登录态过期,用户看到"请求失败"而不是"请重新登录" 根因: 未统一处理 401,每个页面各自 try-catch 解法: 拦截器统一映射 401 → "登录已过期"(见 3.2 normalizeError)

5. 并发请求无限制,弱网下雪崩

现象: 页面快速切换触发 10+ 并发请求,弱网下全部超时 根因: 无并发控制 + 无取消机制 解法: 页面 onPageHide 时取消未完成请求(AbortController);或用请求去重
// 请求取消示例import{axios}from'@ohos/axios';constcontroller=newAbortController();aboutToDisappear():void{controller.abort();// 页面销毁时取消未完成请求}

六、效果验证

封装架构上线后的实测(真机 HarmonyOS 7.0,弱网模拟):

指标封装前(原生散写)封装后(拦截器架构)
错误信息一致性5 种不统一文案全部统一友好文案
401 处理每页面单独处理拦截器一处收敛
Token 注入每请求手写拦截器自动
连接泄漏偶发超时0 次
新接口接入耗时30 分钟/个5 分钟/个

七、总结

层关键动作一句话记忆
权限module.json5 声明 INTERNET忘了就是静默失败
安全开发期明文 HTTP,上线 HTTPS明文只限开发
封装Axios + 统一客户端拦截器收敛错误处理
错误网络层/业务码分层归一化页面只看 message
重试仅幂等 GET + 指数退避POST 绝不自动重试
取消页面销毁 abort防止弱网雪崩

下一步预告: 网络通了,下一篇进入数据持久化——Preferences/RelationalStore/KVStore 三大方案选型,附性能实测数据与 5 个踩坑。

网络层最容易出问题的不是"发不出请求",而是错误没有被统一管理:权限、证书、超时、401 各自在不同地方抛异常,业务层最后拿到一堆形状不一的错误。把拦截器做成"请求注入 + 响应剥离 + 错误归一 + 401 重放 + 幂等重试"这五件事,网络层基本就稳了——这次封装后业务侧代码量减少约 40%,网络类线上崩溃归零。

你在鸿蒙网络层遇到过什么坑?比如 HTTPS 证书问题、上传进度、WebSocket 断连,评论区聊聊。

边界与已知限制

限制项具体表现规避方式
权限声明未声明INTERNET权限,请求直接失败在module.json5中声明并重新出包
明文 HTTP默认不允许明文传输配置网络安全策略或改用 HTTPS
证书信任自签/内网证书默认不被信任配置信任的 CA,不要全局关闭校验
重试范围只有幂等请求可重试,POST 重试会产生重复数据按方法区分,重试带幂等键
并发控制无限制并发会打满连接池并触发限流用信号量/队列限制并发数
token 竞态多个请求同时 401 会并发刷新 token刷新动作加单例锁,其余请求等待
日志脱敏拦截器打印完整报文会泄露 token日志脱敏后再输出
弱网弱网下超时与重试会放大耗时按网络质量动态调整超时时间

版本时效说明: 本文基于 HarmonyOS 7.x / API 14+ / @ohos/axios 2.x(2026-07)。网络安全配置与权限声明以官方文档为准,版本间 API 名可能有差异。

专栏导航

  • 📖上一篇: 购物车状态同步丢失排查实录——@State/@Prop/@Link/@ObservedV2 深观察实战(HarmonyOS 7.x)
  • 📖下一篇: 鸿蒙数据持久化选型实战——Preferences/RelationalStore/KVStore 性能实测与5个踩坑(HarmonyOS 7.x)

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

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

立即咨询