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 实例;
- 注册请求拦截器和响应拦截器;
- 统一提供
request、get、post、delete、patch方法; - 捕获请求异常并调用统一错误处理。
创建实例的代码:
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响应; - 业务码检查失败;
- 服务器返回异常。
它主要负责:
- 关闭 Loading;
- 调用统一错误处理;
- 继续抛出错误,让业务层可以使用
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 类
浙公网安备 33010602011771号