应用提交发布之前,网络异常页值得比正常返回页多看几眼。正常 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 编译和服务器联调。

Logo

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

更多推荐