HarmonyOS 网络层封装实践:打造 axios 风格请求 + 拦截器

前言

原生 @ohos.net.http 每次请求都要 createHttp()request()、手动解析 response.result、再处理错误码,散落在各业务页面既重复又难统一(token 注入、错误统一弹窗、加载态、日志)。在 Web 端我们习惯 axios 的「实例 + 拦截器」模式,HarmonyOS 同样可以照此思路封装一层。本文给出一个可直接落地的 DTRequest 设计:请求/响应拦截器、自动 token 注入、统一错误归一化、TypeScript 泛型返回,并给出在 ArkTS 严格模式下的注意事项(禁用 any/unknown、避免展开运算符等)。

问题描述

未封装时常见的乱象:

  1. 每个页面都写 httpRequest.request(url, {...}),token 写在十几个地方,过期了改不全。
  2. 后端返回 { code: 0, data: ... }{ code: 200, data: ... } 两种约定混用,前端判断各写各的。
  3. 弱网/超时只拿到底层 errcode,用户看到的是毫无意义的「错误 2300001」。
  4. ArkTS 严格模式下用 any 接后端数据,编译报错或类型全失,还得写 @ts-ignore(实际上 ArkTS 不支持)。

目标:把「请求前(加 header、加 loading)、请求后(解包、统一错误)、异常(归一化提示)」三件事全部收敛到一层。

细节解析

1. ArkTS 严格模式的约束

  • 禁止使用 any / unknown:后端数据结构必须用 interface 显式声明,或用 class + 字段默认值。
  • 禁止展开运算符 {...obj}Object.assign 的部分用法在严格场景受限,属性访问要静态可知。
  • 网络层建议用泛型 <T> 表达返回数据类型,编译期拿到类型安全。

2. 拦截器拆成三段

  • requestInterceptor:统一拼 baseURL、注入 Authorization、加 Content-Type、可选择展示全局 loading。
  • responseInterceptor:把后端 { code, data, msg } 归一为 { ok, data, message },非业务成功码抛统一错误。
  • errorInterceptor:把底层 errcode(如超时、无网络)映射成中文提示。

3. 单例 + 泛型

class DTRequest 单例持有配置(baseURL、超时、token 获取器),暴露 get<T> / post<T>。业务侧 const user = await api.get<User>('/user/info') 即可拿到强类型结果。

示例代码(可运行 ArkTS/ArkUI)

示例 1:统一返回结构与拦截器类型

// net/types.ets
export interface ApiResult<T> {
  ok: boolean;
  data: T;
  message: string;
}

export interface RawResponse<T> {
  code: number;
  data: T;
  msg: string;
}

export class ApiError extends Error {
  code: number;
  constructor(code: number, message: string) {
    super(message);
    this.code = code;
  }
}

示例 2:DTRequest 核心封装

// net/DTRequest.ets
import { http } from '@kit.NetworkKit';
import { ApiError, ApiResult, RawResponse } from './types';

type TokenGetter = () => string;

class DTRequest {
  private baseURL: string = 'https://api.example.com';
  private timeout: number = 10000;
  private getToken: TokenGetter = () => '';

  setConfig(cfg: { baseURL: string; timeout?: number; getToken: TokenGetter }): void {
    this.baseURL = cfg.baseURL;
    if (cfg.timeout !== undefined) {
      this.timeout = cfg.timeout;
    }
    this.getToken = cfg.getToken;
  }

  private async request<T>(method: 'GET' | 'POST', path: string, body?: object): Promise<T> {
    const req = http.createHttp();
    const url = this.baseURL + path;
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + this.getToken()
    };
    const options: http.HttpRequestOptions = {
      method: method,
      header: headers,
      timeout: this.timeout,
      extraData: body ? JSON.stringify(body) : undefined
    };
    try {
      const resp = await req.request(url, options);
      if (resp.responseCode !== 200) {
        throw new ApiError(resp.responseCode, `HTTP ${resp.responseCode}`);
      }
      const raw = JSON.parse(resp.result as string) as RawResponse<T>;
      if (raw.code !== 0) {
        throw new ApiError(raw.code, raw.msg || '业务错误');
      }
      return raw.data;
    } catch (e) {
      if (e instanceof ApiError) {
        throw e;
      }
      throw new ApiError(-1, '网络异常,请稍后重试');
    } finally {
      req.destroy();
    }
  }

  get<T>(path: string): Promise<T> {
    return this.request<T>('GET', path);
  }

  post<T>(path: string, body: object): Promise<T> {
    return this.request<T>('POST', path, body);
  }
}

export const api = new DTRequest();

示例 3:业务侧使用(强类型)

// model/User.ets
export interface User {
  id: number;
  name: string;
  avatar: string;
}

// 初始化(应用启动时一次)
api.setConfig({
  baseURL: 'https://api.example.com',
  getToken: () => AppStorage.get<string>('token') ?? ''
});

// 页面调用
@Entry
@ComponentV2
struct Profile {
  @Local user: User | null = null;
  @Local err: string = '';

  aboutToAppear() {
    api.get<User>('/user/info')
      .then((u) => { this.user = u; })
      .catch((e: ApiError) => { this.err = e.message; });
  }

  build() {
    Column() {
      if (this.user) {
        Text(`欢迎,${this.user.name}`)
      } else if (this.err) {
        Text(this.err).fontColor(Color.Red)
      }
    }.padding(20)
  }
}

总结

  • 统一入口:所有请求走 api.get/post,baseURL、超时、token 只在一处配置。
  • 拦截器思维:请求前加 header、响应后解包 code、异常时归一化中文提示,业务页面只关心 data
  • 泛型保类型api.get<User>() 让后端结构在编译期就可见,避免 any 失控(ArkTS 也禁止 any)。
  • 严格模式纪律:显式 interface、不用 any/unknown、避免展开运算符;字段访问静态可知,才能过编译。
  • 资源释放:每次 createHttp() 后务必 destroy(),否则连接泄漏。

一套干净的 DTRequest 能让后续 20 个接口的开发从「复制 30 行样板」变成「一行 api.get<T>()」,同时把 token 失效、弱网提示等全局问题一次性解决。

Logo

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

更多推荐