Network Kit http:弱网重试请求代际隔离与资源释放【鸿蒙心迹】
应用提交发布之前,网络异常页值得比正常返回页多看几眼。正常 Wi-Fi 下点一次“加载状态”,得到成功文案,这只能说明最顺利的分支被走过。用户连续点两次、第一次迟迟不回、第二次已经开始,界面是否还会被旧响应改写?离开页面之后,请求对象有没有释放?这些不属于花哨功能,却很容易把一个原本清楚的审核前自检页面变得不可信。
这里设计一个 ReleaseProbe 演示工具,检查任务固定为 NET-046。演示流程是发起第一轮请求后再发起第二轮,当前请求代际为 rev=2;第一轮标记“已取消”,第二轮处于“待响应”。配置的连接超时为 5000 ms、读取超时为 8000 ms。这些都是演示设定,不是实测响应耗时,也不是平台审核通过依据。

一、真正难处理的是旧响应回到新界面
对一个展示型网络接口,正常流程很简单:创建请求、等待响应、转换状态。问题发生在页面意图变化之后。第一轮请求 A 在移动网络中被拖慢,用户随即重试 B;如果 B 先完成,UI 显示最新结果,随后 A 才返回,就可能把界面又改回旧状态。只把按钮设置成 disabled 并不足以解决这个问题,因为用户仍可能通过页面重入、路由返回或者程序触发另一轮请求。
本文使用两个独立的概念:请求实例和请求代际。实例对应一次 http.createHttp() 返回的对象,负责真实 HTTP 资源;代际是业务层单调增加的整数,用来判定回调是否仍然代表用户最近一次意图。销毁旧请求有助于减少无意义的等待,但不能保证旧异步回调绝不会进入应用代码;所以在更新 UI 之前仍需要比较代际。
华为 HTTP 数据请求开发指导明确说明每个 HttpRequest 对应一个 HTTP 请求任务,不应重复拿同一请求对象执行多次请求。请求结束应主动调用 destroy(),并按需取消响应头监听。本例只用 Promise 的 request(),故意不注册额外监听,减少生命周期分支。使用网络能力需要声明 ohos.permission.INTERNET,示例地址也必须换成研发环境中合法、可控的测试服务。
二、把“释放一次”写成可检查的不变量
下面的 ArkTS 类解决请求替换、异常返回和资源重复销毁的问题。ProbeLease 是应用自己定义的小结构,用 closed 保证清理入口幂等;它不是系统 API。业务对象 RequestProbe 只保留当前活跃租约,旧请求无论成功还是失败都不会拥有更新最新界面的资格。
import { http } from '@kit.NetworkKit';
interface ProbeLease {
request: http.HttpRequest;
closed: boolean;
}
export class RequestProbe {
private rev: number = 0;
private active?: ProbeLease;
private close(lease: ProbeLease): void {
if (lease.closed) return;
lease.closed = true;
lease.request.destroy();
}
async start(url: string): Promise<string> {
const current = ++this.rev;
if (this.active) this.close(this.active);
const lease: ProbeLease = { request: http.createHttp(), closed: false };
this.active = lease;
try {
const response = await lease.request.request(url, {
method: http.RequestMethod.GET,
connectTimeout: 5000,
readTimeout: 8000
});
if (current !== this.rev) return 'STALE';
return response.responseCode === 200 ? 'OK' : 'HTTP_ERROR';
} catch (_) {
return current === this.rev ? 'NETWORK_ERROR' : 'STALE';
} finally {
this.close(lease);
if (this.active === lease) this.active = undefined;
}
}
cancel(): void {
this.rev += 1;
if (this.active) this.close(this.active);
this.active = undefined;
}
}
这里没有把“返回了 Promise”直接等同于业务成功,必须检查 responseCode。连接超时、读取超时是请求参数,不是重试次数;服务端返回 500 与网络层异常应当分别记录。close() 允许取消路径和 finally 都到达同一请求而不会重复调用 destroy(),但如果后续加入事件订阅,还应在销毁前对应调用 off()。生产项目应当把销毁时自身可能抛出的异常也纳入清理策略,不应让日志记录失败盖过最初的请求错误。
三、重试不等于重放所有业务操作
读状态接口可以允许用户重新拉取;支付、提交订单、创建资源等写操作却不能因为“超时了”就无条件再发一次。客户端看到超时,并不代表服务端没有处理成功。对不可重复的业务,必须引入服务端认可的幂等键和结果查询流程,不能拿本文的 rev 当交易凭证。这是网络请求封装与发布前风险审查之间最重要的分界。
ReleaseProbe 只演示 GET 状态查询。请求 A 被 B 取代后,A 在视图层的资格失效;若研发测试脚本模拟旧回调,它应被分类为 STALE,而不是把第二轮状态覆盖掉。表面上看这只是一个整数比较,实际保护的是“用户当前正在等待哪一轮结果”的语义。
四、页面状态要把等待、取消和失败分开
展示页只订阅一个请求管理对象,但 UI 不直接持有 HTTP 内部句柄。下面片段说明点击两次如何形成 rev=2 的业务意图,以及页面离开时如何取消仍然活跃的请求。url 是必须由研发人员提供的服务地址,示例中不会伪装出真实服务器返回。
import { RequestProbe } from '../network/RequestProbe';
@Entry
@Component
struct ReleaseProbePage {
private probe: RequestProbe = new RequestProbe();
private sequence: number = 0;
@State status: string = '待开始';
private async run(url: string): Promise<void> {
const current = ++this.sequence;
this.status = `第${current}轮待响应`;
const result = await this.probe.start(url);
if (current !== this.sequence) return;
this.status = result;
}
aboutToDisappear(): void {
this.sequence += 1;
this.probe.cancel();
}
build() {
Column({ space: 14 }) {
Text('ReleaseProbe · NET-046')
Text(this.status)
Button('运行受控网络测试')
.onClick(() => { this.run('https://example.invalid/probe'); })
}.padding(20)
}
}
这里的 example.invalid 是明确不可用的占位域名,不能直接用来证明网络能力正常。把它替换成授权的受控测试地址后,还需要检查 HTTPS 证书、服务响应和权限声明。sequence 属于 UI 层,rev 属于请求层:前者防止旧结果更新页面,后者防止过期 HTTP 回调被误认成有效结果。两层数字在这个最小示例通常同步递增,但不能假设复杂路由和多个调用方里永远相等。

五、用确定的故障脚本核对状态,而不是捏造耗时
截图中的 NET-046 / rev=2 / 首轮已取消 / 第二轮待响应 是一份设计期“预期状态快照”。为了让 QA 可复现,可以搭建一个受控服务:第一条请求延迟返回,第二条立即受理,再主动触发页面退出和重新进入。测试应分别记录触发次序、服务端是否收到请求、客户端何时取消以及 HiLog 有无过期回调。不能把图里预设的超时阈值写成实际发生的超时。
调试面板建议只保留结构化标识,例如 ReleaseProbe NET-046 rev=1 cancelled、ReleaseProbe NET-046 rev=2 pending、ReleaseProbe NET-046 stale rev=1 ignored。这些字符串是验收脚本的期望日志样例。真实执行前,还需要根据代码实际插桩;当前演示图没有连接远端服务器,也没有证明任何请求确实成功或失败。
为了不让网络页变成“无限转圈”,状态机至少要区分 IDLE、PENDING、OK、HTTP_ERROR、NETWORK_ERROR、STALE。其中 STALE 一般只用于诊断,不应直接显示给普通用户。图中的第二轮尚未收到响应,页面只能写“待响应”,不能擅自写“网络正常”。

六、给异常分类一个固定出口
把状态转换写成纯函数,可以让单元测试覆盖大部分分支,而不用每次都依赖外部网络。下面函数只负责将真实返回结果映射为 UI 可用的状态标签,不控制请求发起;过期判断仍然发生在使用它之前。
export type ProbeStatus = 'OK' | 'HTTP_ERROR' |
'NETWORK_ERROR' | 'STALE';
export function displayStatus(status: ProbeStatus): string {
switch (status) {
case 'OK': return '状态查询成功';
case 'HTTP_ERROR': return '服务器返回异常状态';
case 'NETWORK_ERROR': return '网络不可用或请求超时';
case 'STALE': return '旧请求已忽略';
}
}
真实系统里,网络错误还可能细分为域名解析、证书、连接超时和读取超时。本文没有给各类错误杜撰具体错误码,因为不同系统版本、网络环境和服务端配置可能使分类结果不同。displayStatus 不应代替错误码分析;它只是把技术细节转换成用户可以理解的状态。日志中可以保留非敏感的错误类别,但不应该直接记录令牌、Cookie 或完整业务 URL 查询参数。
七、上架自测不等于平台审核结论
华为关于应用发布运行的体验要求强调,发布阶段需要完成本地自测试,并使功能对用户与审核团队清楚可见。对应到 ReleaseProbe,至少应该在断网、慢网、服务端 4xx/5xx、连续重试、页面退出和重新进入这几条路径下验收:按钮能否恢复,错误解释是否明确,是否有永不消失的 Loading,取消之后是否出现旧结果回填。网络权限与隐私说明则应按应用实际功能另行核对。
本篇能够说明的是设计和开发方法,而不是某个版本已经通过了上架审核。尚未验证的内容包括真机网络抖动、模拟器网络能力、系统对 destroy() 的具体中断时序,以及服务端幂等处理。只有取得可重放的测试记录,才适合把结果写进发布材料。
把一次请求对象、一次用户意图和一次业务提交分开,逻辑就会清楚许多。先保证旧结果不能覆盖当前状态,再验证资源一定释放,最后讨论是否允许重试业务操作。 对上架前的网络自检而言,这比单纯追求一张绿色“请求成功”截图更有价值。
资料依据: 华为开发者文档:HTTP 数据请求;华为开发者文档:发布运行要求。示意日志与图片为预期界面,未经实际 DevEco 编译和服务器联调。
更多推荐



所有评论(0)