尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

HarmonyOS7 网络层怎么封才不烂尾?HttpService、拦截器、重试、缓存一套讲清

HarmonyOS7 网络层怎么封才不烂尾?HttpService、拦截器、重试、缓存一套讲清
📅 发布时间:2026/6/30 14:45:07

文章目录

    • 前言
    • 为什么要统一网络层
    • 拦截器链设计
    • 请求拦截器:Token 注入 + 签名
    • 响应拦截器:错误码处理 + Token 自动刷新
    • 请求重试策略
    • GET 请求缓存 + 过期策略
    • HttpService 主类:把所有东西串起来
    • 业务层用起来
    • 一些实用建议

前言

写过几个鸿蒙项目之后,你会发现一个很痛的问题:网络请求代码散落在各个页面和 ViewModel 里,到处是重复的 Token 拼接、错误处理、loading 状态管理。改一个接口地址要全局搜索替换,加一个签名逻辑要改十几个文件。

这篇文章我把网络层彻底收拢到一个HttpService里,拦截器、重试、缓存一把搞定,后面所有业务都只跟这一个入口打交道。

为什么要统一网络层

分散的网络请求有这些坑:

  • Token 过期了,每个请求各自处理刷新逻辑,容易出现并发刷新

  • 接口报错,有的页面弹 Toast,有的静默失败,体验不一致
  • 弱网环境下没有重试,用户只能手动下拉刷新
  • 同一个 GET 接口短时间内重复请求,浪费流量和服务器资源

统一网络层的核心目标就一个:让业务代码只关心"请求什么数据",不关心"怎么请求"。

拦截器链设计

拦截器思路来自 OkHttp,鸿蒙虽然没有这个库,但模式可以自己实现。核心就是一个数组,请求前走一遍请求拦截器,响应后走一遍响应拦截器。

先定义拦截器接口:

// 拦截器接口定义exportinterfaceHttpInterceptor{onRequest?(config:RequestConfig):Promise<RequestConfig>;onResponse?(response:HttpResponse):Promise<HttpResponse>;onError?(error:HttpError):Promise<HttpError>;}exportinterfaceRequestConfig{url:string;method:string;headers:Record<string,string>;params?:Record<string,Object>;body?:Object;timeout?:number;retryCount?:number;cache?:boolean;cacheTTL?:number;}exportinterfaceHttpResponse{code:number;data:Object;message:string;rawResponse:http.HttpResponse;}exportinterfaceHttpError{code:number;message:string;config:RequestConfig;rawError?:Error;}

请求拦截器:Token 注入 + 签名

请求拦截器最常用的场景就是往 header 里塞 Token 和签名。Token 从 Preferences 里读,签名用时间戳 + AppSecret 做 HMAC。

exportclassAuthInterceptorimplementsHttpInterceptor{privateappSecret:string='your_app_secret';asynconRequest(config:RequestConfig):Promise<RequestConfig>{// 注入 Tokenconsttoken=awaitthis.getToken();if(token){config.headers['Authorization']=`Bearer${token}`;}// 生成签名consttimestamp=Date.now().toString();constsignStr=`${timestamp}${this.appSecret}`;constsign=awaitthis.hmacSha256(signStr);config.headers['X-Timestamp']=timestamp;config.headers['X-Sign']=sign;returnconfig;}privateasyncgetToken():Promise<string|null>{constcontext=getContext(this)ascommon.UIAbilityContext;constprefs=awaitpreferences.getPreferences(context,'auth_store');returnprefs.getSync('access_token','')asstring;}privateasynchmacSha256(data:string):Promise<string>{consthmacAlg=cryptoFramework.createHmac({algName:'sha256'});// 简化示例,实际需要用密钥初始化constresult=awaithmacAlg.update(data);returnresult.toString();}}

响应拦截器:错误码处理 + Token 自动刷新

响应拦截器的重头戏是 Token 刷新。这里有个坑必须处理:多个请求同时收到 401,不能同时发多个刷新请求。用一个 Promise 锁来搞定。

exportclassTokenRefreshInterceptorimplementsHttpInterceptor{privateisRefreshing:boolean=false;privaterefreshPromise:Promise<string>|null=null;asynconResponse(response:HttpResponse):Promise<HttpResponse>{// Token 过期,自动刷新if(response.code===401){constnewToken=awaitthis.refreshToken();// 刷新成功后抛出特殊标记,让 HttpService 重试原始请求throw{code:-1,message:'token_refreshed',retry:true}asHttpError;}// 业务错误码统一处理if(response.code!==200&&response.code!==0){throw{code:response.code,message:response.message||'未知错误',config:{}asRequestConfig}asHttpError;}returnresponse;}privateasyncrefreshToken():Promise<string>{// 防止并发刷新if(this.isRefreshing){returnthis.refreshPromise!;}this.isRefreshing=true;this.refreshPromise=newPromise<string>(async(resolve,reject)=>{try{constcontext=getContext()ascommon.UIAbilityContext;constprefs=awaitpreferences.getPreferences(context,'auth_store');constrefreshToken=prefs.getSync('refresh_token','')asstring;constresult=awaithttp.createHttp().request('https://api.example.com/auth/refresh',{method:http.RequestMethod.POST,extraData:{refresh_token:refreshToken}});constdata=JSON.parse(result.resultasstring)asRecord<string,string>;awaitprefs.put('access_token',data['access_token']);awaitprefs.flush();resolve(data['access_token']);}catch(e){// 刷新失败,踢用户到登录页reject(e);}finally{this.isRefreshing=false;this.refreshPromise=null;}});returnthis.refreshPromise;}}

请求重试策略

弱网环境太常见了,地铁里、电梯里都可能断网。自动重试能显著提升用户体验。我用指数退避策略,第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,最多重试 3 次。

privateasyncrequestWithRetry(config:RequestConfig):Promise<HttpResponse>{constmaxRetries=config.retryCount??3;letlastError:HttpError|null=null;for(letattempt=0;attempt<=maxRetries;attempt++){try{returnawaitthis.doRequest(config);}catch(error){lastError=errorasHttpError;// 只对网络错误重试,业务错误不重试if(!this.isRetryable(errorasHttpError)){throwerror;}if(attempt<maxRetries){constdelay=Math.pow(2,attempt)*1000;// 指数退避awaitthis.sleep(delay);console.info(`[HttpService] 重试第${attempt+1}次,等待${delay}ms`);}}}throwlastError!;}privateisRetryable(error:HttpError):boolean{// 网络超时、连接失败、5xx 服务端错误可以重试returnerror.code===-1||error.code===-2||(error.code>=500&&error.code<600);}privatesleep(ms:number):Promise<void>{returnnewPromise(resolve=>setTimeout(resolve,ms));}

GET 请求缓存 + 过期策略

对于不经常变化的数据(比如配置信息、分类列表),缓存一下能省不少请求。用一个简单的 Map + 过期时间来实现。

interfaceCacheEntry{data:HttpResponse;expireAt:number;}exportclassHttpCacheManager{privatecache:Map<string,CacheEntry>=newMap();privatedefaultTTL:number=5*60*1000;// 默认 5 分钟get(key:string):HttpResponse|null{constentry=this.cache.get(key);if(!entry)returnnull;if(Date.now()>entry.expireAt){this.cache.delete(key);returnnull;}returnentry.data;}set(key:string,data:HttpResponse,ttl?:number):void{this.cache.set(key,{data,expireAt:Date.now()+(ttl??this.defaultTTL)});}// 清除指定前缀的缓存invalidate(prefix:string):void{for(constkeyofthis.cache.keys()){if(key.startsWith(prefix)){this.cache.delete(key);}}}clear():void{this.cache.clear();}}

HttpService 主类:把所有东西串起来

最后把拦截器、重试、缓存组装到一起:

exportclassHttpService{privateinterceptors:HttpInterceptor[]=[];privatecacheManager:HttpCacheManager=newHttpCacheManager();privatebaseUrl:string;constructor(baseUrl:string){this.baseUrl=baseUrl;}addInterceptor(interceptor:HttpInterceptor):HttpService{this.interceptors.push(interceptor);returnthis;}asyncget<T>(url:string,params?:Record<string,Object>,options?:Partial<RequestConfig>):Promise<T>{constconfig:RequestConfig={url:this.baseUrl+url,method:'GET',headers:{},params,...options};// 检查缓存if(config.cache!==false){constcached=this.cacheManager.get(config.url+JSON.stringify(params??{}));if(cached)returncached.dataasT;}constresponse=awaitthis.requestWithRetry(config);// 缓存 GET 响应if(config.cache!==false){this.cacheManager.set(config.url+JSON.stringify(params??{}),response,config.cacheTTL);}returnresponse.dataasT;}asyncpost<T>(url:string,body?:Object,options?:Partial<RequestConfig>):Promise<T>{constconfig:RequestConfig={url:this.baseUrl+url,method:'POST',headers:{'Content-Type':'application/json'},body,...options};constresponse=awaitthis.requestWithRetry(config);returnresponse.dataasT;}privateasyncdoRequest(config:RequestConfig):Promise<HttpResponse>{// 执行请求拦截器链letprocessedConfig=config;for(constinterceptorofthis.interceptors){if(interceptor.onRequest){processedConfig=awaitinterceptor.onRequest(processedConfig);}}// 发起实际请求consthttpRequest=http.createHttp();constresult=awaithttpRequest.request(processedConfig.url,{method:processedConfig.methodashttp.RequestMethod,header:processedConfig.headers,extraData:processedConfig.body??processedConfig.params,connectTimeout:processedConfig.timeout??15000,readTimeout:processedConfig.timeout??15000,});letresponse:HttpResponse={code:result.responseCode,data:JSON.parse(result.resultasstring),message:'',rawResponse:result};// 执行响应拦截器链for(constinterceptorofthis.interceptors){if(interceptor.onResponse){response=awaitinterceptor.onResponse(response);}}returnresponse;}}// 全局单例 + 初始化exportconsthttpService=newHttpService('https://api.example.com').addInterceptor(newAuthInterceptor()).addInterceptor(newTokenRefreshInterceptor());

业务层用起来

封装完之后,业务代码变得特别干净:

// 在 ViewModel 或 Page 中使用interfaceUserInfo{name:string;avatar:string;level:number;}asyncfunctionloadUserInfo(){try{constdata=awaithttpService.get<UserInfo>('/user/profile',undefined,{cache:true,cacheTTL:10*60*1000// 缓存 10 分钟});this.userName=data.name;this.userAvatar=data.avatar;}catch(error){// 错误已经被拦截器处理过,这里只需要关心 UI 降级this.showErrorState=true;}}

一些实用建议

用了这套封装之后,我有几点感受比较深:

拦截器顺序很重要。Token 注入要在签名之前,Token 刷新要在业务错误码处理之前。顺序搞反了会出奇怪的 bug。

重试别太激进。最多 3 次,一定要用指数退避。我见过有人写死循环重试,直接把服务端打爆了。

缓存的 key 要精心设计。简单的 URL + 参数拼接对于大多数场景够用了,但如果参数里有时间戳之类的动态值,要做特殊处理,否则缓存永远命中不了。

Token 刷新的并发控制是关键。不用 Promise 锁的话,一个页面 5 个请求同时 401,就会发 5 个刷新请求,后面的刷新请求用的是已经失效的 refresh_token,全部失败,用户直接被踢到登录页。这个问题我调了一下午才发现。

相关新闻

  • 七人拼团小程序:社交电商新玩法
  • 基因编辑产业化:从科研探索到临床应用,重构生命健康产业底层逻辑
  • 从零到一:SkyWalking 9.x 与 Elasticsearch 8.x 生产环境部署实战

最新新闻

  • 马克·吐温:从密西西比河到世界文坛,一部美国精神的成长史
  • iObjects Java 部署实战:从零到一的避坑指南
  • windows怎么打开后缀为epub的文件
  • 深度解析:如何实现浏览器Cookie安全本地化导出的终极方案
  • 交易所系统开发:搭建指南与功能步骤详解
  • MOE实战:从复合物结构到稳定构象的分子动力学模拟全流程

日新闻

  • 【计算机毕业设计案例】基于 Spring Boot+Vue 的电影售票系统设计与实现 前后端分离架构下影院在线购票管理平台(程序+文档+讲解+定制)
  • 到底 TMD 用哪个: npm, pnpm, Yarn, Bun, Deno? 傻瓜, 当然用 npm 啦
  • Google限制Meta使用Gemini模型 凸显AI授权竞争白热化

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号