Axios封装
2024-08 1
1. 为什么要封装Axios以及封装的注意点?
封装目的:
- 统一get、post请求方式。
- 统一处理请求和响应、错误处理。否则每个需要用到请求的地方,都需要使用try-catch捕捉错误。
- 方便后续拓展。封装了Axios后,后续如果有涉及到接口层的改动,比如说需要在调用的时候增加鉴权字段,增加防重放攻击等,只改一个地方即可。
注意点:
- 对特定 method 封装成新的 API,却暴露极少的参数,如封装GET、POST方法,确只暴露url和param或者data。
- 封装创建axios实例的方法,或者封装自定义axios类。
以上两点在封装的时候应该避免,因为这样会增加理解成本,属于为了封装而封装。
2. 安装 Axios
首先,确保你的项目中已经安装了 Axios
。如果还没有安装,可以使用 npm 或 yarn 进行安装。
npm install axios
# or
yarn add axios
3. 创建 Axios 实例
新建http.js
文件,通过Axios.create
创建一个Axios实例 :
// 创建一个 Axios 实例
const instance = axios.create({
baseURL: "https://api.example.com", // API的基础路径
timeout: 10000, // 请求超时时间
headers: {
"Content-Type": "application/json",
// 其他全局默认请求头
},
});
4. 封装请求拦截器
到这里需要注意一下,由于Axios的get请求和Post请求对于参数的处理稍微有点不同,因此我们可以在封装请求拦截器的时候,统一它们俩的请求方式。
原始的get、post请求方式如下:
// GET 请求方法
axiso.get(url, { params, headers });
// 封装 POST 请求方法
axiso.post(url, data, { headers });
统一之后如下:
// GET 请求方法
axiso.get(url, { params, headers });
// 封装 POST 请求方法
axiso.post(url, { data, headers });
封装拦截器如下:
// 请求拦截器
instance.interceptors.request.use((config) => {
if (config.method === "post") {
const data = config.data;
config = {
...config,
...data,
data: data.data,
};
}
return config;
});
5.封装响应拦截器
const codeMessage = {
200: "服务器成功返回请求的数据。",
201: "新建或修改数据成功。",
202: "一个请求已经进入后台排队(异步任务)。",
204: "删除数据成功。",
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
401: "用户没有权限(令牌、用户名、密码错误)。",
403: "用户得到授权,但是访问是被禁止的。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
406: "请求的格式不可得。",
410: "请求的资源被永久删除,且不会再得到的。",
422: "当创建一个对象时,发生一个验证错误。",
500: "服务器发生错误,请检查服务器。",
502: "网关错误。",
503: "服务不可用,服务器暂时过载或维护。",
504: "网关超时。",
};
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 处理响应数据
return response.data;
},
(error) => {
// 统一处理响应错误
if (error.response) {
// 服务器响应了错误代码
const errorMsg = codeMessage[error.response.status];
console.log(errorMsg);
} else if (error.request) {
// 请求已发出,但没有收到响应
console.error("网络错误");
} else {
// 处理其他错误
console.error("请求错误", error.message);
}
return Promise.resolve(error);
},
);
注意,在上述代码中,第38行return Promise.resolve(error);
通过Promise.resolve()
方法将错误返回给调用方,这样,在调用方的时候就需要根据状态码进行处理即可,无需进行错误处理。
6. 使用封装的 Axios 实例
封装完后,我们在http文件中导出instance
实例:
const { get, post } = instance;
export { get, post };
export default instance;
这样就可以在项目的其他部分直接使用封装好的 instance
进行 API 请求。
import { get, post } from './http.js'
// GET 请求
const res = await get("/demo", { params: { id: 123 } });
// POST 请求
const res = await post("/demo", { data: {id: 123 } });
7. 处理不同环境下的 Base URL
如果项目有多个环境(如开发、测试、生产),可以通过不同环境配置不同的 baseURL
。
// 在 http.js 中
const instance = axios.create({
baseURL: process.env.REACT_APP_API_URL || "https://api.example.com",
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
确保在 .env
文件中为不同的环境配置了 REACT_APP_API_URL
。
# .env.development
REACT_APP_API_URL=https://dev.api.example.com
# .env.production
REACT_APP_API_URL=https://prod.api.example.com
通过这种方式,你可以灵活管理不同环境下的 API 请求。
以上,便是一次简单的Axios封装。接下来,我们继续封装,增加重试机制、取消请求机制、防重放攻击机制、
8. 添加重试机制
重试机制需要用到 axios-retry
插件。
(1)安装axios-retry
pnpm i axios-retry --save
(2)为axios实例添加重试机制
// 添加重试机制
axiosRetry(instance, {
retries: 3, // 设置重试次数
retryDelay: (retryCount) => {
return retryCount * 1000; // 设置重试间隔(每次重试延迟增加)
},
retryCondition: (error: any) => {
// 指定在什么条件下重试,比如5xx错误
return error?.status > 500;
},
});
9. 封装取消请求
先来看看单个请求怎么取消:
import axios from "axios";
import { get } from './http.js'
const { token, cancel } = axios.CancelToken.source();
get("/demo", { cancelToken: token }).then((res) => {
console.log(res);
});
setTimeout(() => {
cancel();
}, 5);
具体逻辑可以看我以前写的一篇文章:前端中断请求的方式与原理
现在来看看封装批量取消请求:
// 1. 创建一个 Map 来存储取消函数
const cancelTokenMap = new Map();
// 请求拦截器
instance.interceptors.request.use((config) => {
// 2. 生成取消请求的Token
const { token, cancel } = axios.CancelToken.source();
config.cancelToken = token;
// 3. 生成一个唯一标识符,例如使用请求的 URL 作为 key,并存储取消函数到 Map
cancelTokenMap.set( config.url, cancel);
if (config.method === "post") {
const data = config.data;
config = {
...config,
...data,
data: data.data,
};
}
return config;
});
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 4. 请求成功后,从 Map 中移除该请求的取消函数
cancelTokenMap.delete(response.config.url);
// 处理响应数据
return response;
},
(error) => {
// 5. 请求失败也要移除取消函数
if (axios.isCancel(error)) {
console.log("请求被取消:", error.message);
} else {
cancelTokenMap.delete(error.config.url);
}
// 统一处理响应错误
if (error.response) {
// 服务器响应了错误代码
const errorMsg = codeMessage[error.response.status];
console.log(errorMsg);
} else if (error.request) {
// 请求已发出,但没有收到响应
console.error("网络错误");
} else {
// 处理其他错误
console.error("请求错误", error.message);
}
return Promise.resolve(error);
}
);
// 6. 封装批量取消请求的函数
function cancelRequests(requestIds = []) {
if (requestIds.length > 0) {
// 取消指定的请求
requestIds.forEach((requestId) => {
if (cancelTokenMap.has(requestId)) {
cancelTokenMap.get(requestId)(); // 调用取消函数
cancelTokenMap.delete(requestId); // 移除该取消函数
}
});
} else {
// 如果没有指定请求 ID,则取消所有请求
cancelTokenMap.forEach((cancel) => cancel());
cancelTokenMap.clear();
}
}
const { get, post } = instance;
export { get, post, cancelRequests };
export default instance;
上述代码中,我们添加了批量请求方式,具体改动如下:
- 创建一个 Map 来存储取消函数。
- 为每个请求创建一个取消令牌。
- 使用请求的 URL 作为唯一标识符,并存储取消函数到 Map,这样后续可以通过传递请求的URL来取消请求。当然,在多个请求的URL相同而参数不同时,取消的就是最后一个发起的URL。
- 请求成功后,从 Map 中移除该请求的取消函数。
- 请求失败也要移除取消函数。
- 封装批量取消请求的函数。
使用方式如下:
// 取消特定请求
cancelRequests(['/demo', '/another-endpoint']);
// 取消所有请求
cancelRequests();
10. 添加防重放攻击机制
重放攻击,就是同一个请求被黑客重复触发,导致数据库多次更新。比如黑客抓包到存钱的接口,然后不断触发接口,那么账户的金额就会不断增加。
要想实现防重放攻击,也就是避免同一个请求被出发两次,就需要给每一个请求打上唯一标识。然后后端维护一个已接收的请求标识符列表(通常有时效性),确保相同标识符不会被重复使用。
import { v4 as uuidv4 } from 'uuid'; // 使用 uuid 来生成唯一标识符
// 请求拦截器
instance.interceptors.request.use((config) => {
// 其他代码...
// 生成防重放攻击的唯一标识符,具体字段名视实际情况而定
config.headers["X-Request-Nonce"] = uuidv4();
// 其他代码...
});
这里需要注意,只使用uuidv4并不安全,最好是将uuidv4进行加密。
11. 完整代码:
import axios from "axios";
import axiosRetry from "axios-retry";
import { v4 as uuidv4 } from "uuid";
// 创建一个 Axios 实例
const instance = axios.create({
baseURL: "/api", // API的基础路径
timeout: 10000, // 请求超时时间
headers: {
"Content-Type": "application/json",
// 其他全局默认请求头
},
});
const codeMessage = {
200: "服务器成功返回请求的数据。",
201: "新建或修改数据成功。",
202: "一个请求已经进入后台排队(异步任务)。",
204: "删除数据成功。",
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
401: "用户没有权限(令牌、用户名、密码错误)。",
403: "用户得到授权,但是访问是被禁止的。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
406: "请求的格式不可得。",
410: "请求的资源被永久删除,且不会再得到的。",
422: "当创建一个对象时,发生一个验证错误。",
500: "服务器发生错误,请检查服务器。",
502: "网关错误。",
503: "服务不可用,服务器暂时过载或维护。",
504: "网关超时。",
};
// 添加重试机制
axiosRetry(instance, {
retries: 3, // 设置重试次数
retryDelay: (retryCount) => {
return retryCount * 1000; // 设置重试间隔(每次重试延迟增加)
},
retryCondition: (error: any) => {
// 指定在什么条件下重试,比如5xx错误
return error?.status > 500;
},
});
// 创建一个 Map 来存储取消函数
const cancelTokenMap = new Map();
// 请求拦截器
instance.interceptors.request.use((config) => {
// 生成取消请求的Token
const { token, cancel } = axios.CancelToken.source();
config.cancelToken = token;
// 使用请求的 URL 作为唯一标识符,并存储取消函数到 Map
cancelTokenMap.set(config.url, cancel);
// 生成防重放攻击的唯一标识符,具体字段名视实际情况而定
config.headers["X-Request-Nonce"] = uuidv4();
if (config.method === "post") {
const data = config.data;
config = {
...config,
...data,
data: data.data,
};
}
return config;
});
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 请求成功后,从 Map 中移除该请求的取消函数
cancelTokenMap.delete(response.config.url);
// 处理响应数据
return response;
},
(error) => {
// 请求失败也要移除取消函数
if (axios.isCancel(error)) {
console.log("请求被取消:", error.message);
} else {
cancelTokenMap.delete(error.config.url);
}
// 统一处理响应错误
if (error.response) {
// 服务器响应了错误代码
const errorMsg = codeMessage[error.response.status];
console.log(errorMsg);
} else if (error.request) {
// 请求已发出,但没有收到响应
console.error("网络错误");
} else {
// 处理其他错误
console.error("请求错误", error.message);
}
return Promise.resolve(error);
}
);
// 封装批量取消请求的函数
function cancelRequests(requestIds = []) {
if (requestIds.length > 0) {
// 取消指定的请求
requestIds.forEach((requestId) => {
if (cancelTokenMap.has(requestId)) {
cancelTokenMap.get(requestId)(); // 调用取消函数
cancelTokenMap.delete(requestId); // 移除该取消函数
}
});
} else {
// 如果没有指定请求 ID,则取消所有请求
cancelTokenMap.forEach((cancel) => cancel());
cancelTokenMap.clear();
}
}
const { get, post } = instance;
export { get, post, cancelRequests };
export default instance;
- 无目录