公交请求韧性·01:HarmonyOS HTTP 错误归一与页面降级【共创稿事节】
一次很有代表性的故障发生在城市公交站的实时到站提示页:网络超时、服务器限流、业务无数据都会被简单显示为“加载失败”,乘客无法判断是否还能信任旧到站时间。乍看只是某个控件或接口的行为不理想,向下追踪却会发现根本原因是数据归属和异常路径没有写成明确约定。

从一次失败现象开始
到站牌一旦与网络发生关系,就出现两个时间概念:数据自身的预测时间和成功接收该预测的本地时间。断网之后,“还有三分钟”并不会因为页面仍显示而继续可信。我们把数据新鲜度标记与具体 Arrival 列表分离,使旧数据可以保留但有明显的陈旧提示。
“HTTP 成功不等于业务成功;网络异常、状态码异常与结构异常需要不同的错误类型”是这篇最重要的设计判断。单看成功路径,城市公交站的实时到站提示页可以靠若干事件回调工作;但只要用户中途修改、取消或重复操作,若没有清晰约束,就无法确定最终输出究竟属于哪一次输入。工程拆分必须以这些不确定性为起点。
我们希望得到的不是一张孤立的效果图,而是一条可追溯的处理链。整个过程拆为发起请求、HTTP 响应、错误归一、旧值降级、状态呈现。每一段只承诺自己负责的结果,下一段不靠猜测前一步的内部状态。对错误归一而言,这种清晰分工也是回归测试能够写得具体的前提。
HTTP 响应码只能说明传输层和服务器处理结果,不能证明每个元素具备 route 与 minutes。解析时先确定返回顶层是数组,再逐项检查必要字段;遇到类型错误应该记录 invalid,而不是强行以零分钟展示,这会造成误导。
分层识别根因
实施选择是:把请求结果包装为显式的成功与失败分支,再将 stale 状态和最后更新时间独立传给页面。这里不能倒过来,先把组件和请求写完,再向现有代码里硬塞判断。规则入口只接收必要数据,输出也尽量可序列化。这样在没有设备的环境下仍能对核心逻辑做确定性检查。
“发起请求”阶段很容易被认为已经完成了整个功能,其实它只提供一个前提。只有进入“HTTP 响应”之后,才能知道前一步是否真的满足业务要求。把这个检查写成显式返回值,比依赖某个内部字段在恰当时刻改变更稳健。
当路径执行到“HTTP 响应”,先区分用户仍在编辑、数据已经校验和动作已经确认三种事实。随后执行“错误归一”时,使用同一份中间结果才能避免旧数据回流。日志或测试也应记录阶段,而不是只看最终页面有没有消失。
把“错误归一”到“旧值降级”视作可中断的边界。任何阶段都可能遇到无效输入或设备状态变化,恢复时应回到上一个仍然可信的事实,而不是为了显示流畅强行跳到最终步骤。这样的路径更适合实际调试。
网络失败和空列表是不同状态。空列表可能意味着当前时段无车辆,失败可能意味着站点查询无法完成;同一张“暂无数据”卡片会让乘客错误推测运行情况。对体验而言,错误分类并非为开发者好看,而是避免提供虚假的到站暗示。

实现一条可控路径
下面给出可以独立检查的关键实现。代码只覆盖本篇要证明的核心行为;涉及工程权限、服务端协议或构建设置的条件已在上下文列明。它应被放入明确的模块中,而不是把所有业务逻辑堆进 ArkUI build()。
type Failure = 'offline' | 'server' | 'invalid' | 'empty'
interface Arrival { route: string; minutes: number }
interface Result { ok: boolean; data: Arrival[]; error?: Failure }
function parseArrivals(status: number, body: string): Result {
if (status < 200 || status >= 300) {
return { ok: false, data: [], error: 'server' }
}
let value: Object | null = null
try { value = JSON.parse(body) as Object }
catch (_err) { return { ok: false, data: [], error: 'invalid' } }
if (!Array.isArray(value)) { return { ok: false, data: [], error: 'invalid' } }
const items = value as Arrival[]
if (items.length === 0) { return { ok: false, data: [], error: 'empty' } }
if (!items.every((x: Arrival) => typeof x.route === 'string' &&
typeof x.minutes === 'number' && Number.isFinite(x.minutes))) {
return { ok: false, data: [], error: 'invalid' }
}
return { ok: true, data: items }
}
请求结束后需要销毁 HttpRequest 对象,这不是可选的性能微调。在网络请求较密集的页面中,长时间持有请求对象会增加资源负担。所有成功和失败路径都应统一收敛到 finally,而不是仅在 happy path 清理。
HTTP 访问层负责把平台请求结果送进 parseArrivals。普通小体量 GET 请求使用 HttpRequest.request;无论成功或失败,finally 都销毁对象。访问前需要按官方配置声明网络权限。
import { http } from '@kit.NetworkKit'
async function queryArrival(url: string): Promise<Result> {
const req = http.createHttp()
try {
const res = await req.request(url, { method: http.RequestMethod.GET,
connectTimeout: 8000, readTimeout: 8000 })
return parseArrivals(res.responseCode, String(res.result))
} catch (_e) {
return { ok: false, data: [], error: 'offline' }
} finally {
req.destroy()
}
}
// entry/src/main/module.json5 中声明 requestPermissions:
// [{ "name": "ohos.permission.INTERNET" }]
代码中的关键判断来自业务约束,而不是某个屏幕比例。以“错误归一”为界,输入应先满足所属阶段的前置条件;当判断失败时,返回错误或保留状态比直接抛出无法解释的页面状态更可控。对需要 UI 反馈的部分,随后由组件转换为明确的文案。
如果把上述核心逻辑接到一个 ArkUI 页面,推荐组件仅处理状态投影和交互事件:例如 TextInput.onChange 接收新的字符串,Button.onClick 触发一次显式动作,得到结果后再设置 @State 驱动展示。对于不是输入框的错误归一功能,则采用相同的单向数据边界而不强行引入 TextInput。

重新构造故障
最应该提前考虑的风险是:不能把缓存数据伪装成实时到站,错误页不能自动关闭可用的离线线路名称。这句话不是发布时随手附加的注意事项,而是编码时必须坚持的限制。只要异常路径缺乏明确处理,下一次回归就可能把一个本来可恢复的状态变成难以判断的失败。
验证的关键是构造可控输入:状态码 503、服务端返回字符串、JSON 为空数组、存在合法一项的数组。先检查纯解析器的返回值,再把结果映射到界面文案,避免把“服务器真的返回了什么”和“页面怎么显示”混为一谈。
重复验收。 连续两次执行HTTP 响应,再执行错误归一,检查第二次动作是新意图还是前次动作的继续。对于错误归一尤其要明确重复处理规则;“按钮点了两次”是用户行为事实,并不自动意味着应当产生两份业务副作用。
错误恢复。 在错误归一成功、旧值降级尚未完成的位置构造异常。应能说明数据暂存在哪里,用户看到何种反馈,是否允许重新触发,以及什么条件下必须停止。不要为了追求一张好看的页面而隐藏正在等待确认的真实状态。
跨版本核对。 在数据和配置持续演进的前提下,固定一份输入,记录预期输出;随后修改不相关的界面颜色或布局,再运行同一用例。如果结果变化,说明错误归一与展示层之间出现了不应存在的隐性耦合。
边界值核对。 不只输入一个“正常例子”,还需要空字符串、用户明确的否定值、最大长度附近以及重复内容等不同形态。对错误归一来说,边界值是否被恰当表达,往往比用户第一次成功完成流程更能反映实现质量。
留下一个可维护的边界
下一篇将讨论反馈提交;那里与只读到站 GET 的根本区别在于副作用。错误归一层仍可复用,但不可以照搬自动重试规则,尤其不能因为状态是 server 就无条件重复一次写入请求。
本篇可用以下结果作为验收目标:断网、HTTP 503、空数组与畸形 JSON 四种输入应输出四种可识别状态。检验时先从固定输入入手,再加入中途切换、重复操作和失败恢复。可解释的错误结果也属于正确实现的一部分;没有必要为了让每个步骤都显示绿色而悄悄跳过不满足的前置条件。
关于不能把缓存数据伪装成实时到站,错误页不能自动关闭可用的离线线路名称这一风险,建议把它写进实际测试用例的名称和断言,而不是只放在代码注释。注释会随着重构失效,测试中的输入与预期结果更容易暴露行为变化。测试本身同样要避免依赖随时变化的网络与时间,否则会把产品故障和测试环境偶然因素混在一起。
当方案需要扩展,优先问“新需求将改变哪个阶段”。如果它只改变HTTP 响应,便不应触及后面的错误归一;如果它改变最终业务语义,规则层和序列化层才需要一起调整。这个决策方式能够避免每加一个按钮就复制一份流程逻辑,也使连载中的代码可以逐篇成长。
对于业务异常,提示文案至少要回答当前状态、可以继续的动作以及不能继续的原因。城市公交站的实时到站提示页里的用户通常不关心内部类名或错误码,但需要知道是等待、重试还是修改输入。把工程错误类别映射为用户可理解的行为建议,比把原始异常堆栈放在对话框里更合适。
交付的验证证据应当与主张相称。纯函数逻辑可以使用固定输入输出来证明;界面效果只能由真实运行或明确标注的示意展示;网络限流、签名、权限等涉及外部系统的结论,需要在对应环境中完成复核。不要把静态检查结果提升为已经完成真实设备测试的结论。
这类问题的适用边界同样清楚:HTTP 成功不等于业务成功;网络异常、状态码异常与结构异常需要不同的错误类型。当具体 SDK、设备能力或服务端协议发生变化,应重新查看对应的官方接口约束,再重新跑上述场景验证,而不是根据一张示例图推断系统行为。
官方资料
更多推荐

所有评论(0)