HarmonyOS 网络请求稳定性实战超时、重试、错误分层与弱网兜底移动端网络问题最麻烦的地方不是接口本身不可用而是它经常表现得“不稳定”地铁里请求转圈、弱网下重复点击、服务端返回了业务错误但页面只提示“失败”、登录失效后多个接口同时弹窗。用户看到的是体验差开发者排查时看到的是一堆零散日志。这篇文章不把网络请求当成一个request()调用来讲而是把它拆成一条可维护链路统一请求入口、超时边界、错误分层、克制重试、弱网缓存兜底和请求留账。示例代码以 ArkTS 写法表达具体 HTTP 能力可按项目实际接入 HarmonyOS 的网络请求 API 或团队已有网络库。1. 先确定这篇文章要解决的网络问题实际项目里网络层常见问题可以先分成四类不要一上来就把所有失败都丢给页面处理。问题用户看到的现象工程上应该做的事请求没有边界loading 长时间不结束统一设置超时并给页面返回可理解状态错误没有分类所有失败都叫“请求失败”区分网络错误、业务错误、鉴权错误、解析错误重试太激进弱网下接口被打爆只重试可恢复错误并使用退避间隔弱网没有兜底页面空白或数据闪没优先保留可用缓存再提示刷新失败本文的目标是让网络层对页面交付一个稳定结果而不是把所有细节都泄漏给 UI。2. 资料边界和项目落点写网络稳定性时建议先看三类资料官方网络请求能力、项目内已有 HTTP 封装、线上错误日志。这里的示例不会绑定某一个业务域名重点放在工程结构。资料用途华为开发者文档中心https://developer.huawei.com/consumer/cn/doc/确认 HarmonyOS 能力入口和 API 版本边界HarmonyOS 指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/查网络、数据、日志等能力的官方说明项目网络封装文件确认是否已经有 baseURL、header、token、错误码处理线上接口日志找出超时、401、业务错误、解析失败的真实比例如果项目已经有网络库不建议推翻重写。更稳妥的做法是在现有网络库外侧加稳定性边界请求模型、错误映射、重试策略和结果记录。3. 请求模型要先收口网络层最怕每个页面自己拼 URL、自己传 header、自己判断错误。先定义请求模型让页面只描述“我要什么”不要关心底层细节。typeHttpMethodGET|POST|PUT|DELETE;interfaceStableRequestTBodyRecordstring,Object{requestId:string;path:string;method:HttpMethod;body?:TBody;timeoutMs:number;retryable:boolean;cacheKey?:string;}interfaceStableResponseTData{requestId:string;data?:TData;fromCache:boolean;costMs:number;}这段代码负责请求边界requestId用来串联日志timeoutMs决定等待上限retryable控制是否允许重试cacheKey表示失败后是否能用缓存兜底。页面不再直接拼接底层参数后面换网络库也不会影响调用方。4. 统一入口处理超时和请求耗时稳定请求的第一步是统一入口。不要让页面自己setTimeout也不要让不同接口有不同的默认等待时间。classStableHttpClient{privatereadonlybaseUrl:string;constructor(baseUrl:string){this.baseUrlbaseUrl;}asyncsendTData(request:StableRequest):PromiseStableResponseTData{conststartAtDate.now();consttaskthis.performRequestTData(request);constdataawaitthis.withTimeout(task,request.timeoutMs,request.requestId);return{requestId:request.requestId,data,fromCache:false,costMs:Date.now()-startAt,};}privateasyncperformRequestTData(request:StableRequest):PromiseTData{consturl${this.baseUrl}${request.path};// 这里替换为项目实际网络请求能力例如 HarmonyOS 网络请求 API 或团队已有 HTTP 封装。returnawaitPromise.resolve(JSON.parse({url:${url}})asTData);}privateasyncwithTimeoutTData(task:PromiseTData,timeoutMs:number,requestId:string):PromiseTData{lettimer0;consttimeoutTasknewPromiseTData((_,reject){timersetTimeout((){reject(newError(NETWORK_TIMEOUT:${requestId}:${timeoutMs}));},timeoutMs);});try{returnawaitPromise.race([task,timeoutTask]);}finally{clearTimeout(timer);}}}这段代码的重点不是Promise.race本身而是把“等多久算失败”变成请求模型的一部分。输入只信任StableRequest失败时抛出带requestId的超时异常下一层ErrorMapper可以继续处理。5. ErrorMapper 不让页面猜错误原因如果网络层只返回Error页面就只能猜。更稳的做法是把错误转换成明确类型。typeNetworkFailureKind|timeout|offline|server|auth|business|decode;interfaceNetworkFailure{requestId:string;kind:NetworkFailureKind;message:string;canRetry:boolean;shouldLogout:boolean;}classErrorMapper{map(request:StableRequest,error:Error):NetworkFailure{constrawerror.message;if(raw.startsWith(NETWORK_TIMEOUT)){return{requestId:request.requestId,kind:timeout,message:当前网络较慢请稍后重试,canRetry:request.retryable,shouldLogout:false,};}if(raw.includes(401)){return{requestId:request.requestId,kind:auth,message:登录状态已过期请重新登录,canRetry:false,shouldLogout:true,};}return{requestId:request.requestId,kind:server,message:服务暂时不可用请稍后再试,canRetry:request.retryable,shouldLogout:false,};}}这一层不直接弹窗也不直接跳登录页。它只负责把底层异常转换成业务可理解的结果能不能重试、是否需要退出登录、用户提示文案是什么。页面只消费明确状态避免每个页面写一套错误判断。6. 重试策略要克制不要把弱网变成雪崩重试不是越多越好。登录、支付、提交订单这类接口不应该随便重试列表、配置、推荐数据可以在可控次数内重试。interfaceRetryDecision{shouldRetry:boolean;delayMs:number;reason:string;}classRetryPolicy{decide(failure:NetworkFailure,attempt:number):RetryDecision{if(!failure.canRetry){return{shouldRetry:false,delayMs:0,reason:request_not_retryable};}if(failure.kindauth||failure.kindbusiness){return{shouldRetry:false,delayMs:0,reason:not_recoverable_${failure.kind}};}if(attempt2){return{shouldRetry:false,delayMs:0,reason:retry_limit_reached};}constdelayMs400*Math.pow(2,attempt);return{shouldRetry:true,delayMs,reason:retry_after_${delayMs}};}}asyncfunctionsleep(delayMs:number):Promisevoid{awaitnewPromisevoid((resolve)setTimeout(resolve,delayMs));}这里把最大重试次数控制在 2 次并且使用退避间隔。它防止的是弱网下接口被页面连续触发也防止业务错误被错误地重复提交。输入来自ErrorMapper下一层可以按RetryDecision决定是否再次请求。7. 弱网兜底不要把旧数据直接当新数据弱网兜底可以提升体验但必须明确告诉页面数据来源。否则用户可能把旧数据当成最新结果。interfaceCachedPayloadTData{data:TData;savedAt:number;expireAt:number;}classNetworkFallbackStoreTData{privatereadonlymemorynewMapstring,CachedPayloadTData();save(cacheKey:string,data:TData,ttlMs:number):void{constnowDate.now();this.memory.set(cacheKey,{data,savedAt:now,expireAt:nowttlMs,});}read(cacheKey:string):CachedPayloadTData|undefined{constcachedthis.memory.get(cacheKey);if(!cached){returnundefined;}if(cached.expireAtDate.now()){this.memory.delete(cacheKey);returnundefined;}returncached;}}这段代码只演示内存兜底真实项目可以替换为 Preferences、RDB 或文件缓存。关键点是savedAt和expireAt页面可以显示“数据来自缓存正在尝试刷新”而不是静默展示旧数据。8. 把请求、错误、重试和缓存串起来单个类很难解决网络稳定性真正起作用的是编排层。classStableNetworkGatewayTData{constructor(privatereadonlyclient:StableHttpClient,privatereadonlymapper:ErrorMapper,privatereadonlyretry:RetryPolicy,privatereadonlyfallback:NetworkFallbackStoreTData){}asyncrequest(request:StableRequest):PromiseStableResponseTData{letattempt0;while(true){try{constresponseawaitthis.client.sendTData(request);if(request.cacheKeyresponse.data!undefined){this.fallback.save(request.cacheKey,response.data,5*60*1000);}returnresponse;}catch(e){constfailurethis.mapper.map(request,easError);constdecisionthis.retry.decide(failure,attempt);if(decision.shouldRetry){attempt1;awaitsleep(decision.delayMs);continue;}if(request.cacheKey){constcachedthis.fallback.read(request.cacheKey);if(cached){return{requestId:request.requestId,data:cached.data,fromCache:true,costMs:Date.now()-cached.savedAt,};}}thrownewError(${failure.kind}:${failure.message});}}}}这段编排代码承担完整边界先请求失败后映射错误再按策略重试最后才读缓存兜底。它防止页面到处写try/catch也让所有网络失败都能被统一记录。9. 请求结果要留账排查时才有线索线上问题最怕只有一句“用户说打不开”。每个请求都应该留下最小可排查记录。interfaceNetworkRecord{requestId:string;path:string;result:success|cache|failed;costMs:number;failureKind?:NetworkFailureKind;retryCount:number;createdAt:number;}classNetworkLedger{privatereadonlyrecords:NetworkRecord[][];append(record:NetworkRecord):void{this.records.push(record);if(this.records.length100){this.records.shift();}}latest():NetworkRecord[]{returnthis.records.slice().reverse();}}这类记录不应该包含 token、手机号、完整地址等敏感信息。它只保留定位问题必要字段接口路径、耗时、失败类型、重试次数和时间。后续如果接入日志系统也可以从这里统一上报。10. 页面层只处理四种状态页面不应该知道底层是超时、DNS、HTTP 还是 JSON 解析它只需要可展示状态。typeNetworkViewStateTData|{type:loading}|{type:content;data:TData;fromCache:boolean}|{type:empty;message:string}|{type:error;message:string;canRetry:boolean};functiontoViewStateTData(response?:StableResponseTData,error?:Error):NetworkViewStateTData{if(response?.data){return{type:content,data:response.data,fromCache:response.fromCache,};}if(error){return{type:error,message:error.message,canRetry:!error.message.includes(auth),};}return{type:empty,message:暂无数据};}页面只围绕loading/content/empty/error渲染弱网缓存通过fromCache告诉用户“当前展示缓存数据”。这样网络层再复杂UI 状态也不会失控。11. 网络稳定性验证动作本地验证不要只测“正常网络能请求成功”还要刻意制造失败场景。验证场景操作方式预期结果接口超时把超时设为 1ms 或 mock 延迟页面结束 loading出现可理解提示可恢复错误mock 网络异常按退避策略最多重试 2 次业务错误mock 业务码失败不重试返回业务提示登录失效mock 401不重试触发统一登录处理弱网缓存先成功一次再断网刷新展示缓存并标记缓存来源建议把这些场景做成调试开关或单元测试用例。网络层越早能复现失败线上排查越少依赖猜测。12. 常见网络问题排查表现象优先检查处理方式loading 不结束是否所有请求都走统一超时禁止页面绕过StableHttpClient重复弹登录401 是否被多个页面各自处理收口到ErrorMapper和登录协调器弱网请求越来越多重试次数和间隔是否可控只对可恢复错误退避重试页面显示旧数据但无提示fromCache是否传到 UI页面增加缓存状态提示排查不到失败接口是否记录requestId和路径在NetworkLedger增加最小留账排查顺序建议从“是否绕过统一入口”开始。如果入口不统一后面的错误分层、重试和日志都会被打散。13. 发布前的网络链路检查发布前不要只看页面是否能打开要留下可复核的网络验收记录。建议每次发版前选 3 个核心接口一个列表接口、一个详情接口、一个提交类接口分别走正常网络、超时、鉴权失效和断网缓存四组场景。所有页面请求必须经过统一网络入口。每个请求都有明确timeoutMs不能无限等待。可重试接口和不可重试接口要分开声明。401、业务错误、解析错误不能混在同一个提示里。弱网兜底数据必须标记来源和过期时间。请求日志不能包含 token、手机号、身份证、详细地址。至少覆盖超时、重试、鉴权失效、缓存兜底四类测试。interfaceNetworkReleaseCheck{apiName:string;normalPassed:boolean;timeoutHandled:boolean;authHandled:boolean;cacheFallbackChecked:boolean;owner:string;}constfeedApiCheck:NetworkReleaseCheck{apiName:feed/list,normalPassed:true,timeoutHandled:true,authHandled:true,cacheFallbackChecked:true,owner:network-module,};这份记录不需要复杂但要能说明核心链路已经被验证。后续如果线上出现“弱网白屏”或“登录弹窗重复”可以直接回看哪一项漏测。网络链路专项证据包把失败分层记录下来弱网问题不能只靠 catch。一次请求失败要能分清楚是 DNS、连接、超时、状态码、业务码还是解析错误。分层越清楚重试和兜底才不会误伤。层级记录字段处理方向连接层networkError判断是否可重试HTTP 层statusCode处理服务异常业务层bizCode展示业务提示解析层parserError回退兼容结构interfaceNetworkFailureEvidence{requestId:stringstage:connect|http|business|parseretryable:booleanmessage:string}functionshouldRetryNetwork(e:NetworkFailureEvidence):boolean{returne.retryable(e.stageconnect||e.stagehttp)}这段代码把失败阶段作为重试输入避免所有错误都机械重试。14. 小结稳定网络层不是多写 catchHarmonyOS 应用里的网络稳定性关键不是在每个页面多写几个catch而是把请求边界提前设计好。统一入口负责超时错误映射负责分类重试策略负责克制缓存兜底负责体验结果留账负责排查。这样写出来的网络层后续接入新接口、改鉴权策略、补弱网体验成本都会低很多。