鸿蒙应用网络层封装实践:统一请求、错误分级与无感重试

举报
yd_227460417 发表于 2026/11/09 01:03:33 2026/11/09
【摘要】 网络请求是鸿蒙应用里最容易写出“面条代码”的地方:页面里直接发请求、错误弹窗满天飞、超时和重试各写一套。本文给一套可落地的网络层封装思路,把请求发送、错误分级、重试和登录态处理收口到一个模块里,适合正在做第一个联网应用的开发者。 一、为什么页面里不该直接发请求在组件里直接调用 http.createHttp 有三个典型问题:请求参数、地址、鉴权头散落在各个页面,改一处要全局搜;错误处理各写各...

网络请求是鸿蒙应用里最容易写出“面条代码”的地方:页面里直接发请求、错误弹窗满天飞、超时和重试各写一套。本文给一套可落地的网络层封装思路,把请求发送、错误分级、重试和登录态处理收口到一个模块里,适合正在做第一个联网应用的开发者。

一、为什么页面里不该直接发请求

在组件里直接调用 http.createHttp 有三个典型问题:

  1. 请求参数、地址、鉴权头散落在各个页面,改一处要全局搜;
  2. 错误处理各写各的,用户一会儿看到 toast、一会儿看到弹窗;
  3. 会话过期后的跳转逻辑重复实现,很容易漏。

网络层封装的目标就是把这三件事收口:页面只关心“调哪个接口、拿什么数据”,其余全部交给统一模块。

二、统一请求入口

先定义一个请求函数,所有业务接口都从它走:

enum ApiError {
  NETWORK = 'NETWORK_ERROR',
  TIMEOUT = 'TIMEOUT',
  UNAUTHORIZED = 'UNAUTHORIZED',
  SERVER = 'SERVER_ERROR',
  BUSINESS = 'BUSINESS_ERROR'
}

interface ApiResult<T> {
  ok: boolean;
  data?: T;
  error?: {
    code: ApiError;
    message: string;
    retryable: boolean;
  };
}

async function request<T>(
  path: string,
  options: RequestOptions
): Promise<ApiResult<T>> {
  const token = await SessionManager.getValidToken();
  const controller = new httpHttpRequest();
  try {
    const resp = await controller.request(`${BASE_URL}${path}`, {
      method: options.method,
      header: {
        'Content-Type': 'application/json',
        Authorization: token ? `Bearer ${token}` : ''
      },
      extraData: options.body ? JSON.stringify(options.body) : undefined,
      connectTimeout: 10000,
      readTimeout: 15000,
      expectDataType: http.HttpDataType.OBJECT
    });
    return parseResponse<T>(resp);
  } catch (e) {
    return parseException(e);
  } finally {
    controller.destroy();
  }
}

两个细节值得注意:

  • http 对象用完必须 destroy,否则连接堆积;
  • 超时时间显式设置,不要依赖默认值,弱网下默认值往往过长。

三、错误分级:先分类,再处理

把所有错误归到四类,处理策略各不相同:

类型 判定 处理策略
网络错误 请求未到达服务器 提示检查网络,可重试
超时 连接或读取超时 静默重试一次,再失败才提示
会话过期 返回 401 清理会话,跳转登录页
业务错误 返回 200 但 code 非 0 直接把服务端 message 透出
function parseResponse<T>(resp: http.HttpResponse): ApiResult<T> {
  if (resp.responseCode === 401) {
    SessionManager.clear();
    emitter.emit('session-expired');
    return { ok: false, error: { code: ApiError.UNAUTHORIZED, message: '登录已过期', retryable: false } };
  }
  if (resp.responseCode >= 500) {
    return { ok: false, error: { code: ApiError.SERVER, message: '服务暂时不可用', retryable: true } };
  }
  const body = resp.result as ResponseBody<T>;
  if (body.code !== 0) {
    return { ok: false, error: { code: ApiError.BUSINESS, message: body.message, retryable: false } };
  }
  return { ok: true, data: body.data };
}

关键设计:会话过期不要在请求函数里直接跳转页面,而是发一个全局事件,由应用级组件统一处理。否则多个并发请求同时过期时,会连续跳转多次。

四、无感重试:只对值得的错误重试

重试最容易写出问题:业务错误也重试、无限重试、重复提交。原则是——只有“结果可预期一致”的请求才自动重试。

async function requestWithRetry<T>(
  path: string,
  options: RequestOptions
): Promise<ApiResult<T>> {
  let result = await request<T>(path, options);
  if (result.ok || !result.error?.retryable) {
    return result;
  }
  await sleep(1000);
  return request<T>(path, options);
}

三条边界必须守住:

  • GET 可以自动重试,POST 默认不重试,除非服务端提供了幂等键;
  • 重试只做一次,失败就交给用户决策;
  • 重试前加短暂延迟,避免服务端刚恢复就被打爆。

五、并发请求的会话刷新

多个请求同时遇到 401 时,如果每个都去刷新会话,必然冲突。标准做法是加一把“刷新锁”:

let refreshing: Promise<string> | null = null;

async function ensureToken(): Promise<string> {
  const token = await SessionManager.getValidToken();
  if (token) {
    return token;
  }
  if (!refreshing) {
    refreshing = refreshSession().finally(() => {
      refreshing = null;
    });
  }
  return refreshing;
}

所有请求入口都走 ensureToken,第一个过期请求负责刷新,其余请求等待同一个 Promise,天然去重。

六、页面侧的正确姿势

封装完成后,页面代码会变得非常薄:

async loadBills() {
  this.loading = true;
  const result = await requestWithRetry<Bill[]>('/bills', { method: 'GET' });
  this.loading = false;
  if (result.ok) {
    this.bills = result.data;
  } else if (result.error?.code !== ApiError.UNAUTHORIZED) {
    promptAction.showToast({ message: result.error?.message ?? '加载失败' });
  }
}

注意这里对 UNAUTHORIZED 的静默处理:跳转登录页已经由全局事件接管,页面不再重复弹提示。

七、几个容易踩的坑

  • 在 build 或 aboutToAppear 里发请求但不处理组件销毁:页面已切走,回调还在写状态,轻则报错重则崩溃,请求要与组件生命周期绑定;
  • 把 http 实例做成全局单例复用:并发请求会互相干扰,一次请求一个实例更安全;
  • 错误提示直接透传服务端原文:服务端文案往往不适合直接展示,重要错误要有本地文案兜底;
  • 忘记处理“成功但数据为空”:列表页要区分“加载失败”和“确实没有数据”,两者的空态 UI 不一样。

八、小结

网络层封装的本质是“把变化收口”:地址会变、鉴权会变、文案会变,但“请求-分类-处理”的骨架不变。先把统一入口和错误分级做扎实,重试和会话并发是后面自然长出来的能力。

建议从自己应用里最痛的那个接口开始重构:把它接到统一入口上,跑通后再批量迁移,风险最小,收益立竿见影。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。