18-网络一断页面就空白-用结果态和重试链路兜住异常

📅 2026/7/11 11:44:08
18-网络一断页面就空白-用结果态和重试链路兜住异常
第18篇网络一断页面就空白用结果态和重试链路兜住异常摘要网络失败不能变成空白页。页面只等接口成功、没有 loading/error/empty/data 结果态时断网、超时和返回乱序都会让用户卡住。更稳的做法是把网络结果收敛成统一状态页面只接收最新请求并给失败态明确的重试入口。我遇到过课程详情页断网后直接空白重新联网也不会恢复。排查后发现页面只有成功分支失败时 catch 里只打了日志没有更新 UI。用户看到的不是网络错误而是什么都没有。这篇文章会把问题拆成几个可落地的点用 loading、empty、data、error 四类结果态替代空白页。服务层把异常收敛成页面可渲染结果。页面用 requestVersion 只接收最新请求。失败态提供重试入口必要时展示缓存摘要。适用环境和边界本文讨论网络读取类页面重点是结果态、请求乱序、失败重试和缓存兜底。示例以课程详情页说明列表、搜索、资料页也适用。维度建议边界服务层把接口成功、空数据和异常转成统一结果页面层按结果态渲染不吞掉失败乱序控制只让最新请求更新 UI兜底策略失败可重试缓存只做辅助提示不展开内容不讨论具体 HTTP 客户端封装网络错误如果只写进日志用户看到的就是空白。错误必须进入页面状态。先把故障链路复盘出来断网空白通常不是接口一个人的问题而是页面只有成功分支。阶段表面现象更深的原因应该看到的结果加载中转圈后页面空白loading 结束但没有 error 状态失败态可见断网catch 后只打日志异常没有转成结果显示错误文案和重试切换数据旧请求覆盖新内容请求返回乱序只接收最新版本缓存失败时展示旧数据不说明缓存和实时结果混淆标明缓存摘要把结果态写出来以后页面就不会在失败时掉进没有分支的空白区域。先定 owner再写代码网络页面的 owner 分工要清楚Service 负责把异常变成结果Page 负责展示Cache 只负责可选兜底。对象归属不要越界做的事CourseApi发请求并返回原始响应决定页面文案CourseService把响应转成结果态直接操作组件Page按状态渲染和重试解析原始异常码CacheRepository保存和读取缓存摘要伪装成实时数据Logger记录失败摘要替代用户可见错误态失败态进入 UI 后日志才是辅助信息而不是唯一的错误处理。先定义统一结果态网络链路里这一步要把不可控的请求结果变成页面能稳定消费的状态。exporttypeNetworkStateT|{kind:loading}|{kind:empty;message:string}|{kind:data;data:T}|{kind:error;message:string;retryable:boolean}结果态让页面不用猜当前是加载、空、成功还是失败。每个状态都有可渲染的信息。服务层收敛异常网络链路里这一步要把不可控的请求结果变成页面能稳定消费的状态。exportclassNetworkCourseService{constructor(privatereadonlyapi:CourseApi){}asyncloadCourse(courseId:string):PromiseNetworkStateCourseDetail{try{constdetailawaitthis.api.fetchCourse(courseId)if(detail.lessons.length0){return{kind:empty,message:当前课程还没有内容}}return{kind:data,data:detail}}catch(err){return{kind:error,message:网络不可用请检查连接后重试,retryable:true}}}}服务层把异常转成结果态页面就不需要在多个 catch 里拼错误文案。页面只接受最新请求网络链路里这一步要把不可控的请求结果变成页面能稳定消费的状态。EntryComponentstruct CourseDetailPage{Statestate:NetworkStateCourseDetail{kind:loading}privaterequestVersion:number0privateservice:NetworkCourseServicecreateCourseService()privateasyncreload(courseId:string):Promisevoid{constversionthis.requestVersionthis.state{kind:loading}constnextawaitthis.service.loadCourse(courseId)if(versionthis.requestVersion){this.statenext}}}请求版本号能避免用户切换课程或重试时较早返回的失败结果覆盖较新的成功结果。失败态给重试入口网络链路里这一步要把不可控的请求结果变成页面能稳定消费的状态。BuilderprivateErrorState(message:string,retryable:boolean){Column({space:12}){Text(message).fontColor($r(app.color.text_secondary))if(retryable){Button(重新加载).onClick(()this.reload(this.courseId))}}.padding(24)}失败态必须是用户能操作的界面。只打日志或只弹一次 Toast都不能让用户继续完成任务。build 中按状态渲染网络链路里这一步要把不可控的请求结果变成页面能稳定消费的状态。build(){Column(){if(this.state.kindloading){LoadingPanel()}elseif(this.state.kindempty){EmptyPanel({message:this.state.message})}elseif(this.state.kinderror){this.ErrorState(this.state.message,this.state.retryable)}else{CourseDetailContent({detail:this.state.data})}}.width(100%).height(100%)}页面只有一个state不会再出现 loading 结束了但 data 为空、error 有值但页面仍显示旧内容的分裂状态。离线兜底读取缓存摘要网络链路里这一步要把不可控的请求结果变成页面能稳定消费的状态。exportclassCourseOfflineFallback{constructor(privatereadonlycache:CourseCacheRepository){}asyncloadCachedSummary(courseId:string):PromiseCourseSummary|null{constcachedawaitthis.cache.get(courseId)returncached?.summary??null}}如果业务允许可以在失败态里显示上次缓存的摘要但要明确标注不是最新数据避免用户误解。用反向搜索把旧路径清掉网络修复后搜索空 catch、散落 loading 和没有重试的失败路径。rgcatch \(|try \{|requestVersion|retry|LoadState|NetworkStateentry features common rgloading|errorText|EmptyPanel|ErrorStateentry features common rgfetchCourse|CourseApi|cacheentry features common重点看 catch 后是否只打日志、页面是否有 error 分支、旧请求是否可能覆盖新请求。验证路径要覆盖真实操作网络验证要把失败当成主流程的一部分。正常联网进入详情页确认 data 状态展示。断网进入确认 error 状态和重试按钮出现。恢复网络后点击重试确认页面能回到 data。快速切换两个课程确认旧请求不覆盖新结果。接口返回空列表确认 empty 状态而不是错误态。存在缓存时断网确认缓存提示不会伪装成最新数据。这组验证不要只在一个入口上跑。很多问题在主流程里看不出来切换页面、返回、清空数据、改主题、断网、换宽屏后才会暴露。风险和回退方案风险触发场景处理方式错误只进日志用户看到空白把异常转成 error 状态旧请求覆盖快速切换或重试使用 requestVersion缓存误导用户失败时展示旧摘要明确标注缓存来源重试无效重试仍用旧参数重试读取当前页面参数回退方案的价值不是保守而是让一次修复能分阶段落地。先保住主路径再删除旧逻辑排查时才不会把多个变量混在一起。常见问题和处理方式现象常见原因处理方式断网后空白没有 error 分支统一结果态重试后仍失败使用旧请求参数重试时读取当前 state空数据显示错误empty 和 error 混用服务层区分空结果切换内容错乱请求乱序只接收最新版本这些问题看起来分散本质上都和 owner、状态来源、刷新时机和验证范围有关。只修一个按钮通常不够要让同一类路径都走回同一套规则。这次问题给我的复盘网络失败要被建模成页面状态。loading、empty、data、error 都有明确 UI重试和缓存兜底也有 owner。复盘这类问题时我会先把用户看到的现象落到工程 owner 上再看代码是不是沿着同一条边界走。只要 owner 不清后面就会变成谁顺手谁修owner 清楚以后验证也能回到具体路径而不是靠感觉判断。真正落到项目里我会用这份小清单收尾先把用户看到的现象对应到具体 owner避免只改表面 UI。再确认写入、读取、刷新、路由或资源是否沿着同一条链路。搜索旧入口、旧 key、硬编码值或临时输出能解释的保留解释不通的收口。用真实入口走一遍主路径再补空态、失败态、返回、重启或宽屏路径。把剩余风险写清楚下一次维护时就不用重新猜当时为什么这么改。小结网络失败要变成可见状态网络失败要被建模成页面状态。loading、empty、data、error 都有明确 UI重试和缓存兜底也有 owner。 这类问题最终都要回到 owner、代码边界和真实路径验证不能只看某个片段是否写得顺眼。。3. 搜索旧入口、旧 key、硬编码值或临时输出能解释的保留解释不通的收口。4. 用真实入口走一遍主路径再补空态、失败态、返回、重启或宽屏路径。5. 把剩余风险写清楚下一次维护时就不用重新猜当时为什么这么改。小结网络失败要变成可见状态网络失败要被建模成页面状态。loading、empty、data、error 都有明确 UI重试和缓存兜底也有 owner。 这类问题最终都要回到 owner、代码边界和真实路径验证不能只看某个片段是否写得顺眼。