Axios 网络请求封装

Axios 网络请求封装

这套封装主要用于项目中的本地相机 HTTP 请求,例如查询相机信息、获取教程数据、查询固件信息等。云存储模块使用的是另一套 HTTP 封装,本文不讨论云存储部分。

1. 为什么要封装 Axios

如果每个页面都直接使用 Axios,请求代码通常会重复处理很多事情:

创建 Axios 实例
设置 baseURL
设置超时时间
设置请求头
打印请求日志
显示和隐藏 Loading
检查登录状态
判断 HTTP 状态码
判断业务状态码
处理网络错误
弹出错误提示

这些逻辑如果散落在页面中,会带来几个问题:

  • 每个页面写一套,代码重复;
  • 有的页面处理了错误,有的页面没有处理;
  • Loading 可能忘记关闭;
  • 请求日志格式不统一;
  • 修改服务器地址或超时时间时需要改很多地方;
  • 后续增加 token、重试、统一错误码时改动范围很大。

封装之后,页面或业务类只需要关心三件事:

请求什么地址
使用 GET 还是 POST
传什么参数

通用逻辑统一放在请求实例和拦截器中处理。

2. 当前项目的封装结构

当前请求封装可以分成三层:

业务请求类
    -> axiosClient.get() / post()
    -> AxiosHttpRequest
    -> @ohos/axios

对应文件作用如下:

文件 作用
ApiService.ets 保存基础地址和请求地址常量
AxiosHttp.ets 封装 Axios 实例、请求方法和拦截器注册
AxiosRequest.ets 创建项目实际使用的 Axios 客户端,配置请求/响应拦截器
AxiosResponse.ets 定义通用响应数据结构
CameraSettingHttp.ets 按业务封装具体请求
FirmwareUpgradeHttp.ets 按业务封装固件相关请求
Index.ets 统一导出 HTTP 模块

日常开发一般不直接修改 AxiosHttpRequest,而是复用 axiosClient,再新增一个业务请求类。

3. 第一层:AxiosHttpRequest 做什么

AxiosHttp.ets 中的 AxiosHttpRequest 是基础请求类,主要负责:

  • 接收请求配置;
  • 创建 Axios 实例;
  • 注册请求拦截器和响应拦截器;
  • 统一提供 requestgetpostdeletepatch 方法;
  • 捕获请求异常并调用统一错误处理。

创建实例的代码:

constructor(options: HttpRequestConfig) {
  this.config = options;
  this.interceptorHooks = options.interceptorHooks;
  this.instance = axios.create(options);
  this.setupInterceptor();
}

这里的好处是:业务层不用重复创建 Axios 实例,所有请求都可以复用同一套配置和拦截器。

4. HttpRequestConfig 扩展了什么

项目没有直接使用 Axios 的原始配置,而是在 InternalAxiosRequestConfig 基础上增加了业务配置:

export interface HttpRequestConfig extends InternalAxiosRequestConfig {
  showLoading?: boolean;
  checkResultCode?: boolean;
  checkLoginState?: boolean;
  needJumpToLogin?: boolean;
  interceptorHooks?: InterceptorHooks;
}

这些字段不是 HTTP 标准字段,是项目为了统一处理业务增加的开关。

showLoading

是否显示请求 Loading:

showLoading: true

请求开始时显示 Loading,请求成功或失败时隐藏 Loading。

适合耗时较长、用户需要等待结果的操作。不建议所有后台静默请求都打开,否则页面会频繁闪 Loading。

checkResultCode

是否检查响应体中的业务状态码:

checkResultCode: true

当前响应拦截器会先判断 HTTP 状态码是否为 200,然后在开启该配置时检查:

response.data.errorCode != 0

如果业务码不为 0,会显示错误信息,并将请求转为 rejected 状态。

checkLoginState

是否在请求发送前检查登录状态:

checkLoginState: true

当前实现从 Preferences 中读取登录状态。如果没有登录,会抛出:

请登录

needJumpToLogin

表示登录失效后是否需要跳转登录页:

needJumpToLogin: true

当前代码中跳转登录的位置还是 TODO,暂时只保留配置字段和判断逻辑,不能把它当成已经实现了自动跳转。

5. 两个拦截器分别做什么

项目中的两个主要拦截器是:

请求拦截器 requestInterceptor
响应拦截器 responseInterceptor

另外每个拦截器还配置了对应的异常处理函数:

requestInterceptorCatch
responseInterceptorCatch

完整流程如下:

调用 axiosClient.get/post
        ↓
请求拦截器
        ↓
发送 HTTP 请求
        ↓
响应拦截器
        ↓
返回结果或进入响应异常处理

6. 请求拦截器

请求拦截器在真正发送请求之前执行:

requestInterceptor: async (config) => {
  // 打印请求方法、地址、参数
  // 显示 Loading
  // 检查登录状态
  // 最后返回 config
  return config;
}

当前项目的请求拦截器主要做四件事。

6.1 打印请求信息

当前会打印:

  • 请求方法;
  • 请求地址;
  • params 参数;
  • data 请求体。

示例日志格式:

网络请求Request 请求方法: POST
网络请求Request 请求链接: http://相机地址,+/请求路径
网络请求Request Params: {...}
网络请求Request Data: {...}

排查接口问题时,先看请求拦截器日志,可以确认请求到底有没有发出去,以及参数是否正确。

注意:不要在日志中输出密码、token、完整手机号或其他敏感数据。当前代码会打印参数,新增敏感字段时应先脱敏。

6.2 显示 Loading

调用方设置:

showLoading: true

请求拦截器执行:

if (config.showLoading) {
  showLoadingDialog();
}

Loading 的关闭由响应成功、响应异常或请求异常统一处理。

6.3 检查登录状态

调用方设置:

checkLoginState: true

请求拦截器会读取 Preferences 中保存的登录状态:

if (config.checkLoginState) {
  const hasLogin = await PreferencesUtil.get(
    StorageConstants.USER_LOGIN,
    false
  );

  if (!hasLogin) {
    throw new Error('请登录');
  }
}

如果请求不需要登录,不要打开这个开关。

6.4 返回或中断请求

请求拦截器最后必须返回配置:

return config;

如果检查失败,可以抛出异常,让请求进入异常流程:

throw new Error('请登录');

7. 请求拦截器异常处理

请求拦截器异常处理函数是:

requestInterceptorCatch: (err) => {
  if (axiosClient.config.showLoading) {
    hideLoadingDialog();
  }
  return err;
}

它处理的是“请求还没有真正发出去之前”的错误,例如:

  • 登录状态校验失败;
  • 请求配置处理异常;
  • 请求拦截器中的异步逻辑异常。

它的重点是关闭 Loading,并继续把错误交给后续 Promise 处理。

8. 响应拦截器

响应拦截器在服务器返回结果后执行:

responseInterceptor: (response) => {
  // 关闭 Loading
  // 打印响应
  // 判断 HTTP 状态码
  // 可选检查业务状态码
  // 返回 response 或 reject
}

当前主要做四件事。

8.1 关闭 Loading

if (axiosClient.config.showLoading) {
  hideLoadingDialog();
}

8.2 打印响应结果

LogUtil.info(
  TAG,
  '网络请求响应Response:' + JsonUtils.stringify(response.data)
);

排查接口问题时,可以把请求日志和响应日志配对查看:

请求参数是否正确
服务器是否返回
HTTP 状态码是多少
业务错误码是多少

8.3 判断 HTTP 状态码

当前实现只将 response.status === 200 视为成功:

if (response.status === 200) {
  return Promise.resolve(response);
}

return Promise.reject(response);

因此,即使服务器返回了响应,只要不是 200,也会进入失败流程。

8.4 判断业务状态码

当请求配置打开 checkResultCode 时,响应拦截器会继续检查业务码:

const config = response.config as HttpRequestConfig;

if (config.checkResultCode && response.data.errorCode != 0) {
  ToastUtil.showToast(response.data.errorMsg);
  return Promise.reject(response);
}

这里要区分两个概念:

HTTP 状态码
= 网络层是否成功

业务状态码
= 服务器业务逻辑是否成功

HTTP 200 不一定代表业务成功,具体还要看是否开启了 checkResultCode

9. 响应拦截器异常处理

响应拦截器异常处理函数是:

responseInterceptorCatch: (error) => {
  hideLoadingDialog();
  errorHandler(error);
  return Promise.reject(error);
}

它处理的是响应阶段的异常,例如:

  • 网络连接失败;
  • 请求超时;
  • 200 响应;
  • 业务码检查失败;
  • 服务器返回异常。

它主要负责:

  1. 关闭 Loading;
  2. 调用统一错误处理;
  3. 继续抛出错误,让业务层可以使用 catch 处理。

10. AxiosHttpRequest 中的统一异常处理

AxiosHttpRequest.request() 外层还有一层异常处理:

request<T = CommonType>(
  config: HttpRequestConfig
): HttpPromise<T> {
  return new Promise((resolve, reject) => {
    this.instance
      .request<CommonType, HttpResponse<T>>(config)
      .then((res) => {
        resolve(res);
      })
      .catch((err: CommonType) => {
        LogUtils.error(
          '网络请求Request异常:',
          err.toString() + ',url:' + config.url
        );
        errorHandler(err);
        reject(err);
      });
  });
}

所以当前错误处理链路大致是:

请求失败
  -> 响应异常拦截器
  -> errorHandler
  -> request() 的 catch
  -> 业务层 catch

调用方仍然建议保留 try/catch,因为统一错误处理只负责通用行为,页面还需要决定失败后显示什么内容或更新什么状态。

11. errorHandler 做什么

统一错误方法位于 AxiosRequest.ets

export function errorHandler(error: CommonType) {
  // 统一处理 AxiosError
}

当前逻辑包括:

  • 识别 AxiosError
  • 关闭 Loading;
  • 按错误码处理特殊情况;
  • 在短时间内过滤重复错误;
  • 弹出错误提示;
  • 对特定相机通信错误释放相机协议。

当前有重复错误过滤逻辑:

let lastError: string | undefined;
let lastErrorTime: number = 0;
const ERROR_RESET_INTERVAL = 60 * 1000;

作用是避免同一个错误在短时间内连续弹出多次提示。

12. 项目中实际使用的客户端

项目实际导出的请求客户端是 AxiosRequest.ets 中的默认对象:

const axiosClient = new AxiosHttpRequest({
  baseURL: 'http://相机地址',
  timeout: 20 * 1000,
  checkResultCode: false,
  headers: {
    'Content-Type': 'application/json'
  },
  interceptorHooks: {
    // 请求拦截器
    // 响应拦截器
  }
});

export default axiosClient;

业务类通过默认导入复用它:

import axiosClient from './AxiosRequest';

13. 最简单的 GET 请求

如果只是发送一个 GET 请求,可以这样写:

import axiosClient from './AxiosRequest';

interface DemoData {
  id: number;
  name: string;
}

async function getDemoData(): Promise<void> {
  try {
    const response = await axiosClient.get<DemoData>({
      baseURL: 'http://相机地址',
      url: '/api/示例路径',
      params: {
        pageIndex: 1,
        pageSize: 20
      },
      showLoading: false,
      checkResultCode: false
    });

    const data = response.data;
    console.info(`请求成功: ${JSON.stringify(data)}`);
  } catch (error) {
    console.error(`请求失败: ${JSON.stringify(error)}`);
  }
}

其中:

  • baseURL:服务器基础地址;
  • url:具体请求路径;
  • params:GET 查询参数;
  • response.data:响应数据;
  • showLoading:是否显示 Loading;
  • checkResultCode:是否检查业务码。

14. 最简单的 POST 请求

POST 请求的数据放在 data 中:

import axiosClient from './AxiosRequest';

interface DemoResponse {
  success: boolean;
  message: string;
}

async function postDemoData(): Promise<void> {
  try {
    const response = await axiosClient.post<DemoResponse>({
      baseURL: 'http://相机地址',
      url: '/api/示例路径',
      data: {
        value: '示例值'
      },
      showLoading: true,
      checkResultCode: false
    });

    console.info(`请求成功: ${JSON.stringify(response.data)}`);
  } catch (error) {
    console.error(`请求失败: ${JSON.stringify(error)}`);
  }
}

15. 需要登录校验的请求

const response = await axiosClient.get<ResponseData>({
  baseURL: 'http://相机地址',
  url: '/api/示例路径',
  checkLoginState: true,
  needJumpToLogin: false,
  showLoading: true
});

当前 needJumpToLogin 还没有真正实现页面跳转,所以如果登录失效,业务层应自行处理异常和页面跳转。

16. 需要检查业务码的请求

如果服务器返回类似下面的结构:

interface ApiResponse<T> {
  errorCode: number;
  errorMsg: string;
  data: T;
}

可以打开业务码检查:

const response = await axiosClient.get<ApiResponse<DemoData>>({
  baseURL: 'http://相机地址',
  url: '/api/示例路径',
  checkResultCode: true
});

errorCode != 0 时,响应拦截器会提示 errorMsg,并让 Promise 进入失败状态。

17. 推荐的业务请求类写法

不要让页面到处直接拼接请求配置,建议每类业务单独封装一个请求类:

import axiosClient from './AxiosRequest';

export interface DemoItem {
  id: number;
  title: string;
}

export interface DemoPageResponse {
  code: number;
  message: string;
  count: number;
  data: DemoItem[];
}

export class DemoHttp {
  async getList(pageIndex: number = 1,
               pageSize: number = 20) {
    return axiosClient.get<DemoPageResponse>({
      baseURL: 'http://相机地址',
      url: '/api/示例列表路径',
      params: {
        pageIndex,
        pageSize
      },
      showLoading: true,
      checkResultCode: false
    });
  }
}

export const demoHttp = new DemoHttp();

页面只调用业务方法:

const response = await demoHttp.getList(1, 20);
const list = response.data.data;

这样页面不需要知道:

  • Axios 实例怎么创建;
  • baseURL 怎么配置;
  • Loading 怎么显示;
  • 错误怎么处理;
  • 参数放在 params 还是 data

18. ApiService 的作用

请求路径不要直接散落在各个页面,统一放到 ApiService.ets

export class ApiService {
  static readonly DEMO_LIST = '/api/示例列表路径';
  static readonly DEMO_DETAIL = '/api/示例详情路径';
}

业务类使用:

import { ApiService } from './ApiService';

return axiosClient.get({
  baseURL: 'http://相机地址',
  url: ApiService.DEMO_LIST
});

这样修改接口路径时只需要修改一处,也可以避免不同页面写出不一致的路径。

19. AxiosResponse 的作用

对于重复使用的响应结构,可以定义泛型响应类:

export class PageResponse<T> {
  code: number = 200;
  message: string = '';
  count: number = 0;
  data: T[] = [];
}

请求时传入具体数据类型:

const response = await axiosClient.get<PageResponse<DemoItem>>({
  baseURL: 'http://相机地址',
  url: ApiService.DEMO_LIST
});

const list: DemoItem[] = response.data.data;

泛型的好处是:

  • 不需要到处使用 any
  • 编辑器可以提示字段;
  • 修改数据结构时更容易发现问题;
  • 页面拿到的数据类型更清晰。

20. GET、POST、DELETE、PATCH 的快速写法

基础类已经封装了常见请求方法:

await axiosClient.get({
  baseURL: 'http://相机地址',
  url: '/api/示例路径',
  params: {}
});

await axiosClient.post({
  baseURL: 'http://相机地址',
  url: '/api/示例路径',
  data: {}
});

await axiosClient.delete({
  baseURL: 'http://相机地址',
  url: '/api/示例路径',
  data: {}
});

await axiosClient.patch({
  baseURL: 'http://相机地址',
  url: '/api/示例路径',
  data: {}
});

请求方法内部只是设置 config.method,然后统一走 request()

get(config: HttpRequestConfig) {
  config.method = 'GET';
  return this.request(config);
}

21. 请求失败时怎么排查

建议按照下面顺序排查:

第一步:看请求拦截器日志

确认:

  • 请求方法是否正确;
  • baseURL 是否正确;
  • url 是否正确;
  • GET 参数是否放在 params
  • POST 参数是否放在 data
  • 请求是否开启了登录校验。

第二步:看响应拦截器日志

确认:

  • 是否收到服务器响应;
  • HTTP 状态码是否为 200
  • 响应体字段是否符合预期;
  • 是否因为 checkResultCode 被判定为失败。

第三步:看 errorHandler 日志

确认:

  • 是否是超时;
  • 是否是网络不可达;
  • 是否是相机连接已经断开;
  • 是否触发了特殊错误码处理。

第四步:检查相机网络

如果请求地址是相机局域网地址,还要确认:

  • 手机是否连接相机 Wi-Fi;
  • 当前应用是否被绑定到了错误的网络;
  • 相机 IP 和端口是否正确;
  • WifiConnectManager 是否把应用切到了普通 Wi-Fi 或手机流量;
  • 相机 TCP 连接是否已经释放或断开。

22. 当前封装的注意事项

22.1 axiosClient.config.showLoading 是共享状态

当前请求拦截器会写入:

axiosClient.config.showLoading = config.showLoading;

如果同时发起多个请求,可能出现下面的情况:

请求 A 开启 Loading
请求 B 不开启 Loading
请求 B 修改共享配置
请求 A 返回时无法准确判断是否需要关闭 Loading

所以当前封装不适合依赖共享 config.showLoading 来精确管理并发请求。后续如果经常并发请求,建议直接使用当前请求的 config.showLoading,或者改成 Loading 计数器。

22.2 不要在多个地方重复处理同一个错误

当前拦截器、request()errorHandler() 都可能参与错误处理。新增逻辑时注意不要重复弹 Toast,否则一次错误可能弹出多次提示。

推荐分工:

拦截器:处理通用 Loading 和日志
errorHandler:处理通用错误码和通用提示
业务层:处理当前页面的特殊状态

22.3 不要把具体业务接口写进基础层

基础层只负责:

  • Axios 实例;
  • 请求配置;
  • 拦截器;
  • 错误处理;
  • 通用响应模型。

具体业务请求放到独立的业务类中,不要把文件删除、固件查询等具体接口直接写进 AxiosHttp.ets

22.4 不要把云请求和相机本地请求混用

当前项目至少存在两套请求体系:

commons/base/http
= 相机本地 HTTP 请求

features/mine_cloud/core/http
= 云端业务请求

修改时先确认调用方属于哪条链路,避免把云服务器地址、认证逻辑或云接口配置混入相机本地请求客户端。

22.5 请求路径和 baseURL 分开维护

建议:

baseURL:统一服务器或相机地址
url:具体接口路径

不要在每个页面里拼完整 URL:

// 不建议
url: 'http://相机地址/api/示例路径'

推荐:

baseURL: 'http://相机地址',
url: ApiService.DEMO_LIST

23. 推荐的新增请求流程

以后新增一个网络功能时,按下面的顺序处理:

1. 在 ApiService 中增加路径常量
2. 在 AxiosResponse 中定义响应模型
3. 新建或修改对应业务 HTTP 类
4. 通过 axiosClient.get/post 发起请求
5. 根据需要设置 showLoading
6. 根据响应结构决定是否设置 checkResultCode
7. 页面通过业务类调用,不直接拼请求
8. 用 try/catch 处理页面自己的失败状态

示例:

// 1. 路径常量
export class ApiService {
  static readonly DEMO_LIST = '/api/示例路径';
}

// 2. 响应模型
export interface DemoResponse {
  code: number;
  message: string;
  data: string[];
}

// 3. 业务请求类
export class DemoHttp {
  async getData() {
    return axiosClient.get<DemoResponse>({
      baseURL: 'http://相机地址',
      url: ApiService.DEMO_LIST,
      showLoading: false,
      checkResultCode: false
    });
  }
}

// 4. 页面调用
try {
  const result = await new DemoHttp().getData();
  console.info(JSON.stringify(result.data));
} catch (error) {
  console.error(JSON.stringify(error));
}

24. 一句话记忆

AxiosHttp.ets
= 封装 Axios 基础能力

AxiosRequest.ets
= 创建项目实际使用的客户端和两个拦截器

ApiService.ets
= 管理请求地址

AxiosResponse.ets
= 管理响应数据类型

业务 Http 类
= 管理具体业务请求

页面
= 只调用业务 Http 类
posted @ 2026-09-01 18:36  带头大哥d小弟  阅读(9)  评论(0)    收藏  举报