在早高峰大量乘客反复刷新公交到站预估中,最难的往往不是画出一个按钮,而是保证按钮背后的业务事实始终可靠。一遇 429 就立刻重发使限流更严重,刷新按钮和后台刷新还互相争抢配额。这个问题如果在原型阶段不拆开,联调以后就会成为难以复现的状态错乱。

技术主题封面

需求从哪里变复杂

在早高峰,用户反复拉动刷新并不会让公交预测更准,反而可能让客户端集中发送大量请求。服务端返回 429 时,应理解为当前请求频率不被接受,立即重复相同请求只会加重限流。

“限流是服务端的节流信号,重试只能在有次数、时间和意图预算的条件下进行”是这篇最重要的设计判断。单看成功路径,早高峰大量乘客反复刷新公交到站预估可以靠若干事件回调工作;但只要用户中途修改、取消或重复操作,若没有清晰约束,就无法确定最终输出究竟属于哪一次输入。工程拆分必须以这些不确定性为起点。

我们希望得到的不是一张孤立的效果图,而是一条可追溯的处理链。整个过程拆为响应分类、可重试判定、计算等待、预算扣减、接受新响应。每一段只承诺自己负责的结果,下一段不靠猜测前一步的内部状态。对限流退避而言,这种清晰分工也是回归测试能够写得具体的前提。

退避的第一原则是区分状态码。429 和部分临时性 5xx 可以考虑重试;参数错误、授权失败或应用解析错误通常应直接处理,而不是进入重试循环。重试条件必须是明确白名单。

先固定数据边界

实施选择是:结合 Retry-After 及带随机抖动的指数退避,使用全局请求预算限制同一站点的并行刷新。这里不能倒过来,先把组件和请求写完,再向现有代码里硬塞判断。规则入口只接收必要数据,输出也尽量可序列化。这样在没有设备的环境下仍能对核心逻辑做确定性检查。

从“响应分类”走到“可重试判定”时,不要直接复用页面当前显示的全部状态。界面可能正在更新,用户也可能继续操作。此处应只传递本阶段约定的数据,并记录这份数据属于哪次动作,减少跨阶段读到不同版本的风险。

“可重试判定”阶段很容易被认为已经完成了整个功能,其实它只提供一个前提。只有进入“计算等待”之后,才能知道前一步是否真的满足业务要求。把这个检查写成显式返回值,比依赖某个内部字段在恰当时刻改变更稳健。

当路径执行到“计算等待”,先区分用户仍在编辑、数据已经校验和动作已经确认三种事实。随后执行“预算扣减”时,使用同一份中间结果才能避免旧数据回流。日志或测试也应记录阶段,而不是只看最终页面有没有消失。

指数退避增长很快,因此需要上限;加入小范围随机抖动是为了避免所有客户端在相同秒数再次冲击服务端。若存在合法 Retry-After,应按照协议解析并优先考虑,不能仅把它当普通字符串。

限流退避的关键设计

让代码成为规则的证据

下面给出可以独立检查的关键实现。代码只覆盖本篇要证明的核心行为;涉及工程权限、服务端协议或构建设置的条件已在上下文列明。它应被放入明确的模块中,而不是把所有业务逻辑堆进 ArkUI build()。

interface Budget { attempts: number; maxAttempts: number }
function waitMs(attempt: number, retryAfterMs: number | null): number {
  if (retryAfterMs !== null && retryAfterMs >= 0) return retryAfterMs
  const base = Math.min(8000, 400 * Math.pow(2, attempt))
  return Math.round(base * (0.8 + Math.random() * 0.4))
}
function canRetry(code: number, budget: Budget): boolean {
  if (budget.attempts >= budget.maxAttempts) return false
  return code === 429 || code === 503 || code === 504
}
// 等待前后都要检查页面是否仍有效;Retry-After 需支持秒数与 HTTP 日期。

预算至少有两个维度:最大尝试次数和总等待上限。只检查次数,用户可能在一次长超时与几次等待后仍困在进度圈中。对于公交到站这类时效性服务,旧数据早已失去意义时应该停止自动尝试。

退避函数需要被实际请求循环调用。这里用 loadStatus 注入一次可控制的请求,避免将 HTTP 细节和重试算法混写;超过预算之后必须明确退出。

function delay(ms: number): Promise<void> {
  return new Promise<void>((resolve) => { setTimeout(resolve, ms) })
}
async function loadWithBudget(loadStatus: () => Promise<number>): Promise<number> {
  const budget: Budget = { attempts: 0, maxAttempts: 3 }
  while (budget.attempts < budget.maxAttempts) {
    const code = await loadStatus()
    budget.attempts += 1
    if (!canRetry(code, budget)) return code
    await delay(waitMs(budget.attempts, null))
  }
  return 429
}
// 真正上线还应加入页面取消令牌和总等待时间预算。

代码中的关键判断来自业务约束,而不是某个屏幕比例。以“计算等待”为界,输入应先满足所属阶段的前置条件;当判断失败时,返回错误或保留状态比直接抛出无法解释的页面状态更可控。对需要 UI 反馈的部分,随后由组件转换为明确的文案。

如果把上述核心逻辑接到一个 ArkUI 页面,推荐组件仅处理状态投影和交互事件:例如 TextInput.onChange 接收新的字符串,Button.onClick 触发一次显式动作,得到结果后再设置 @State 驱动展示。对于不是输入框的限流退避功能,则采用相同的单向数据边界而不强行引入 TextInput。

限流退避的运行界面或工程路径

错误不应藏在界面后面

最应该提前考虑的风险是:不是所有失败都适合重试,用户主动离开页面后计划中的延迟任务必须停止。这句话不是发布时随手附加的注意事项,而是编码时必须坚持的限制。只要异常路径缺乏明确处理,下一次回归就可能把一个本来可恢复的状态变成难以判断的失败。

要构造固定序列 429→429→200、429→400、503→超时,并检查每种序列的退出条件。测试关注逻辑是否尊重预算,不是在没有可控服务端的情况下编造网络耗时结果。

边界值核对。 不只输入一个“正常例子”,还需要空字符串、用户明确的否定值、最大长度附近以及重复内容等不同形态。对限流退避来说,边界值是否被恰当表达,往往比用户第一次成功完成流程更能反映实现质量。

状态验收。 在早高峰大量乘客反复刷新公交到站预估中,专门制造一个尚未完成的状态:一遇 429 就立刻重发使限流更严重,刷新按钮和后台刷新还互相争抢配额。观察模块是否明确阻止下一动作、是否保留用户已经输入的有效内容,以及重新操作后是否会得到可解释的新结果。这个检查的价值在于不把一次成功操作当作稳定性的全部证据。

逆序验收。 把正常操作反过来执行,例如先触发接受新响应,再补齐预算扣减所需要的条件。若界面没有把错误留在错误位置,或者隐式使用上一轮结果,就说明阶段约束并未完全落实。真正的测试应覆盖操作顺序变化,而非只按产品示例步骤点击。

重复验收。 连续两次执行响应分类,再执行响应分类,检查第二次动作是新意图还是前次动作的继续。对于限流退避尤其要明确重复处理规则;“按钮点了两次”是用户行为事实,并不自动意味着应当产生两份业务副作用。

怎样确认这套设计可靠

退避与导航离开也有关。乘客在等待期间切换站点,原站点的重试任务不应继续占据额度;用页面代际序号或明确的取消令牌让未发生的尝试失效。

本篇可用以下结果作为验收目标:构造 429→429→200,确认总尝试数被预算限制且旧结果不会越权更新。检验时先从固定输入入手,再加入中途切换、重复操作和失败恢复。可解释的错误结果也属于正确实现的一部分;没有必要为了让每个步骤都显示绿色而悄悄跳过不满足的前置条件。

交付的验证证据应当与主张相称。纯函数逻辑可以使用固定输入输出来证明;界面效果只能由真实运行或明确标注的示意展示;网络限流、签名、权限等涉及外部系统的结论,需要在对应环境中完成复核。不要把静态检查结果提升为已经完成真实设备测试的结论。

还有一个容易被忽视的协作问题:页面状态、领域规则和最终输出往往由不同代码文件甚至不同开发者维护。要让改动可控,必须为限流退避明确最小接口,规定哪些数据可以跨模块传递、哪些只是展示层的临时变量;代码评审只要看到两处重复决定同一事实,就需要回到单一来源原则。

调试不应从增加大量打印开始。先把响应分类 → 可重试判定 → 计算等待 → 预算扣减 → 接受新响应按顺序列出来,每次只在边界处记录不含个人信息的阶段与结果类别。这样即使没有真机性能指标,仍能通过构造输入与检查返回值定位哪一步发生分歧;反过来,如果只看最终页面颜色,就难以证明中间没有错误。

关于不是所有失败都适合重试,用户主动离开页面后计划中的延迟任务必须停止这一风险,建议把它写进实际测试用例的名称和断言,而不是只放在代码注释。注释会随着重构失效,测试中的输入与预期结果更容易暴露行为变化。测试本身同样要避免依赖随时变化的网络与时间,否则会把产品故障和测试环境偶然因素混在一起。

限流退避的扩展场景与异常路径

这类问题的适用边界同样清楚:限流是服务端的节流信号,重试只能在有次数、时间和意图预算的条件下进行。当具体 SDK、设备能力或服务端协议发生变化,应重新查看对应的官方接口约束,再重新跑上述场景验证,而不是根据一张示例图推断系统行为。

官方资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐