news 2026/7/15 22:18:31

26-页面返回后请求重复发-用请求门闩和结果序号收口

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
26-页面返回后请求重复发-用请求门闩和结果序号收口

第26篇|页面返回后请求重复发:用请求门闩和结果序号收口

摘要:页面返回后请求重复发,通常不是网络层问题,而是页面生命周期、返回刷新和手动重试同时触发了同一批加载。HarmonyOS 页面里如果aboutToAppear、返回回调、下拉刷新都直接调用 Service,慢网下会出现三次相同请求,最后还可能让旧结果覆盖新结果。更稳的做法是用请求 key 合并同条件请求,再用结果序号保证页面只消费最新返回。

实际项目里我见过一个列表页:从详情页返回时,列表会自动刷新;用户又顺手下拉一次,日志里同一个接口发了三次。网络快时看不出来,网络慢时旧请求最后返回,反而把刚更新的列表覆盖掉。继续在每个入口加if loading不够,因为不同筛选条件、不同页面实例和手动重试都需要被区分。

这篇文章解决四个实际问题:

  1. 如何判断重复请求来自生命周期、返回刷新还是手动重试。
  2. 如何用请求 key 表达一次加载的业务条件。
  3. 如何合并同条件请求,并避免旧结果覆盖新结果。
  4. 如何用日志和弱网路径验证请求确实被收口。


先把重复请求拆成三类

重复请求不能只看接口次数,要看触发来源。

触发来源常见现象正确处理
页面首次出现aboutToAppear自动加载允许加载,但要带请求 key
从详情页返回返回后希望看到最新数据只在数据可能变化时刷新
手动下拉刷新用户明确要求重新拉取可以绕过短缓存,但仍要有序号
慢网旧结果返回后发请求先成功,先发请求后成功页面只消费最新序号

这个拆分能避免把所有入口都粗暴包在一个loading里。loading只能挡住当前页面的交互,不知道两个请求是不是同一组业务条件。

请求 key 要包含业务条件

请求 key 不要只写页面名。筛选条件、分页、用户域、刷新来源都会影响结果。

exportinterfaceCourseListQuery{userId:stringkeyword:stringcategoryId:stringpageNo:number}exportfunctionbuildCourseListKey(query:CourseListQuery):string{return['course_list',query.userId,query.categoryId,query.keyword.trim(),`${query.pageNo}`].join('|')}

这段代码的边界是“同样条件才算同一个请求”。关键词不同、分类不同、页码不同,就不能合并;条件完全相同,返回刷新和手动刷新才有机会复用。

RequestGate 合并同条件请求

请求门闩只处理并发合并,不替代业务 Service。它保存当前正在执行的 Promise,同一个 key 进来时直接复用。

exportclassRequestGate<T>{privaterunning:Map<string,Promise<T>>=newMap()run(key:string,task:()=>Promise<T>):Promise<T>{constcurrent=this.running.get(key)if(current){returncurrent}constnext=task().finally(()=>{this.running.delete(key)})this.running.set(key,next)returnnext}}

这里不要把结果长期缓存进 Gate。Gate 只解决“同一时间重复发”的问题,数据缓存是否存在、缓存多久,仍然应该由 Service 或 Repository 决定。

页面用序号消费最新结果

只合并请求还不够。不同条件的请求可能同时存在,页面要保证旧结果不能覆盖新状态。

@Entry@Componentstruct CourseListPage{@Staterows:CourseRowVm[]=[]@Stateloading:boolean=false@StateerrorText:string=''privatequerySeq:number=0privateservice:CourseListService=createCourseListService()asyncloadCourses(query:CourseListQuery):Promise<void>{constseq=++this.querySeqthis.loading=truetry{constrows=awaitthis.service.loadRows(query)if(seq!==this.querySeq){return}this.rows=rowsthis.errorText=''}catch(err){if(seq===this.querySeq){this.errorText='课程加载失败,请重试'}}finally{if(seq===this.querySeq){this.loading=false}}}}

querySeq是页面自己的消费边界。它不关心网络层发了几次,只保证当前页面不会把过期结果写回 UI。

Service 决定返回是否需要重拉

从详情页返回后不一定要重拉。更稳的做法是让写入动作更新数据版本,列表页返回时只在版本变化时加载。

exportclassCourseListService{privategate:RequestGate<CourseRowVm[]>=newRequestGate()constructor(privatereadonlyrepository:CourseRepository,privatereadonlyadapter:CourseRowAdapter){}asyncloadRows(query:CourseListQuery):Promise<CourseRowVm[]>{constkey=buildCourseListKey(query)returnthis.gate.run(key,async()=>{constrecords=awaitthis.repository.list(query)returnthis.adapter.toRows(records)})}}

页面只传查询条件,Service 负责读取和转换。这样以后增加缓存、分页或离线兜底,不需要到每个页面入口里补判断。

用日志确认只发了一次

排查重复请求时,日志要带 key、序号和来源。

rg"loadCourses\(|buildCourseListKey|RequestGate|querySeq"entry features common rg"aboutToAppear|onPageShow|onRefresh|back"entry features common

运行时日志建议记录source=appear/back/refreshkey=course_list|...seq=3。如果同一个 key 在 200ms 内出现多次,就先看是否绕过了 Gate。

验证清单

  1. 首次进入列表,只发一次当前查询请求。
  2. 进入详情后返回,没有数据变化时不重拉。
  3. 详情修改课程后返回,列表刷新一次。
  4. 返回瞬间下拉刷新,同条件请求被合并。
  5. 快速切换分类,旧分类结果不会覆盖新分类。
  6. 弱网失败后错误态可见,点击重试只发一次。

真实项目里最容易漏掉的入口

重复请求经常不是主入口制造的,而是几个“看起来合理”的辅助入口叠在一起。

入口为什么容易漏建议处理
列表页aboutToAppear每次返回都会触发只表达加载意图,交给 RequestGate 判断
详情页保存后返回需要刷新最新数据保存成功后只更新数据版本,不直接拉列表
下拉刷新用户主动触发可以绕过短缓存,但仍复用同 key 并发
筛选切换条件变化快用序号丢弃旧结果
网络失败重试用户会连续点按 key 合并重试中的请求

我会把这些入口全部列出来,再看每个入口最终是否都走loadCourses(query)。如果某个入口直接调用 Repository 或网络层,就说明请求边界还没有收口。

给请求留下可排查的上下文

重复请求问题要能从日志里还原出来。只记录“请求开始”不够,至少要带来源、key、序号和结果。

exportinterfaceRequestLogContext{source:'appear'|'back'|'refresh'|'retry'key:stringseq:number}exportfunctionformatRequestLog(ctx:RequestLogContext,state:string):string{return`[course-list]${state}source=${ctx.source}key=${ctx.key}seq=${ctx.seq}`}

有了这组字段,排查时就能判断是同一个 key 被重复触发,还是不同筛选条件的正常请求。发布前再按key聚合日志,能很快发现某个返回路径是不是仍在绕过门闩。

我会怎样复现这个问题

复现重复请求时,不要只看 Network 面板里有没有两条接口。更稳的复现顺序是:先打开列表页等首次加载完成;进入详情页修改一条数据;返回列表页的同时快速下拉刷新;再切换一次筛选条件。这个组合能同时触发首次缓存、返回刷新、手动刷新和条件变化。如果日志里同一个 key 出现两次以上,就说明门闩没有接住;如果不同 key 的旧结果覆盖当前列表,就说明页面序号没有生效。

复现时我会记录三项结果:

  1. 同一个请求 key 在一次返回路径里出现了几次。
  2. 最后写入页面状态的是哪个seq
  3. 失败重试有没有绕过RequestGate

小结:返回刷新要有条件,不靠生命周期硬拉

页面返回后请求重复发,核心不是少写一个生命周期回调,而是给每次加载建立可解释的边界:请求 key 表达业务条件,RequestGate 合并同条件并发,页面序号只消费最新结果,Service 决定数据怎么读取。这样首次进入、返回刷新、手动重试和弱网乱序都能被解释。
后写入页面状态的是哪个seq
3. 失败重试有没有绕过RequestGate

小结:返回刷新要有条件,不靠生命周期硬拉

页面返回后请求重复发,核心不是少写一个生命周期回调,而是给每次加载建立可解释的边界:请求 key 表达业务条件,RequestGate 合并同条件并发,页面序号只消费最新结果,Service 决定数据怎么读取。这样首次进入、返回刷新、手动重试和弱网乱序都能被解释。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 22:09:45

分布式事务决策框架:从 2PC 到 Saga 的方案对比与工程实践

分布式事务决策框架&#xff1a;从 2PC 到 Saga 的方案对比与工程实践 系统从单体迁移到微服务后&#xff0c;订单服务、库存服务、支付服务、通知服务需要同时成功或同时失败。这时候&#xff0c;分布式事务成了绕不开的难题。2PC、TCC、Saga、本地消息表……方案很多&#xf…

作者头像 李华
网站建设 2026/7/15 21:58:52

AI 服务错误归因:推理失败到底该怪模型还是基础设施

AI 服务错误归因&#xff1a;推理失败到底该怪模型还是基础设施 一、背景与问题 AI 推理服务的错误分类是一个被低估的问题。一次推理请求返回 500&#xff0c;错误日志可能写着 "inference failed"。但这个 "failed" 的含义太宽了&#xff1a;可能是 GPU …

作者头像 李华
网站建设 2026/7/15 21:56:06

Go Channel内部实现:从环形缓冲区到select的随机唤醒策略

Go Channel内部实现&#xff1a;从环形缓冲区到select的随机唤醒策略 一、Channel在Go并发模型中的定位 Go的并发哲学是"不要通过共享内存来通信&#xff0c;而要通过通信来共享内存"。Channel是实现这一哲学的核心原语&#xff0c;它既是goroutine之间的数据管道&am…

作者头像 李华