目录
前言
在当今的前端开发领域,数据交互是不可或缺的一环。Axios 作为一款基于 Promise 的 HTTP 客户端,因其简洁的 API 和丰富的配置选项,深受广大开发者的喜爱。然而,在实际项目中,我们常常会遇到一个问题:重复发送相同的请求,这不仅会浪费网络资源,还可能导致服务器压力增大。为了提高应用的性能和用户体验,我们需要对 Axios 进行封装,以避免重复请求的问题。
本文将详细介绍两种封装 Axios 的方案,帮助您有效地避免重复请求,提升项目质量。我们将从实际场景出发,分析问题原因,并提供具体的实现步骤和代码示例。无论您是初入前端的新手,还是经验丰富的开发者,相信都能从中获得启发。
让我们一起探索如何优雅地封装 Axios,让数据交互更加高效、稳定!
Demo
项目demo地址:cancelRequest: 避免重复调用接口的demo https://gitee.com/zhp26zhp/cancel-request
第一种实现方法
通过生成请求的唯一标识符(key),来避免重复发送相同的请求。(发布订阅模式)
定义了一个EventEmitter
类,用于实现发布订阅模式。它允许在请求成功或失败时,通知所有订阅了该请求的回调函数。
javascript
class EventEmitter {
constructor() {
this.event = {}
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]]
} else {
this.event[type].push([cbres, cbrej])
}
}
emit(type, res, ansType) {
if (!this.event[type]) return
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve') {
cbArr[0](res)
}else{
cbArr[1](res)
}
});
}
}
}
生成请求Key
javascript
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
存储已发送但未响应的请求
javascript
const pendingRequest = new Set();
const ev = new EventEmitter()
请求拦截器中,在请求发送前,生成请求Key,并检查是否已有相同的请求在等待响应。如果有,则通过发布订阅模式挂起该请求,直到收到响应。如果没有,则将请求Key添加到pendingRequest
中。
javascript
instance.interceptors.request.use(async (config) => {
let hash = location.hash
let reqKey = generateReqKey(config, hash)
if(pendingRequest.has(reqKey)) {
let res = null
try {
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject)
})
return Promise.reject({
type: 'limiteResSuccess',
val: res
})
}catch(limitFunErr) {
return Promise.reject({
type: 'limiteResError',
val: limitFunErr
})
}
}else{
config.pendKey = reqKey
pendingRequest.add(reqKey)
}
return config;
}, function (error) {
return Promise.reject(error);
});
响应拦截器中,在请求成功或失败时,通过handleSuccessResponse_limit
和handleErrorResponse_limit
函数处理响应,并发布订阅通知
javascript
instance.interceptors.response.use(function (response) {
handleSuccessResponse_limit(response)
return response;
}, function (error) {
return handleErrorResponse_limit(error)
});
将成功响应的结果发布给所有订阅了该请求的回调函数。
javascript
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(response))
}catch(e) {
x = response
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'resolve')
delete ev.reqKey
}
}
将错误响应的结果发布给所有订阅了该请求的回调函数。
javascript
function handleErrorResponse_limit(error) {
if(error.type && error.type === 'limiteResSuccess') {
return Promise.resolve(error.val)
}else if(error.type && error.type === 'limiteResError') {
return Promise.reject(error.val);
}else{
const reqKey = error.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(error))
}catch(e) {
x = error
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'reject')
delete ev.reqKey
}
}
return Promise.reject(error);
}
完整代码如下:
javascript
import axios from "axios"
let instance = axios.create({
baseURL: 'http://localhost:3001', // api 的 base_url
timeout: 50e3, // request timeout
})
// 发布订阅
class EventEmitter {
constructor() {
this.event = {}
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]]
} else {
this.event[type].push([cbres, cbrej])
}
}
emit(type, res, ansType) {
if (!this.event[type]) return
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve') {
cbArr[0](res)
}else{
cbArr[1](res)
}
});
}
}
}
// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()
// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
let hash = location.hash
// 生成请求Key
let reqKey = generateReqKey(config, hash)
if(pendingRequest.has(reqKey)) {
// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
let res = null
try {
// 接口成功响应
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject)
})
return Promise.reject({
type: 'limiteResSuccess',
val: res
})
}catch(limitFunErr) {
// 接口报错
return Promise.reject({
type: 'limiteResError',
val: limitFunErr
})
}
}else{
// 将请求的key保存在config
config.pendKey = reqKey
pendingRequest.add(reqKey)
}
return config;
}, function (error) {
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 将拿到的结果发布给其他相同的接口
handleSuccessResponse_limit(response)
return response;
}, function (error) {
return handleErrorResponse_limit(error)
});
// 接口响应成功
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(response))
}catch(e) {
x = response
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'resolve')
delete ev.reqKey
}
}
// 接口走失败响应
function handleErrorResponse_limit(error) {
if(error.type && error.type === 'limiteResSuccess') {
return Promise.resolve(error.val)
}else if(error.type && error.type === 'limiteResError') {
return Promise.reject(error.val);
}else{
const reqKey = error.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(error))
}catch(e) {
x = error
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'reject')
delete ev.reqKey
}
}
return Promise.reject(error);
}
export default instance;
调用接口例子
javascript
export function getWetherByOneExample() {
return instance.get('/data')
}
第二种方法(axios版本0.22.0以上)
isCancel
:从axios
中提取用于判断请求是否被取消的方法。cacheRequest
:用于存储请求的AbortController
实例,以便后续取消请求。
javascript
const { isCancel } = axios;
const cacheRequest = {};
abortCacheRequest
:根据请求的唯一标识reqKey
取消请求。
javascript
function abortCacheRequest(reqKey) {
if (cacheRequest[reqKey]) {
console.log("abortCacheRequest", reqKey);
cacheRequest[reqKey].abort();
delete cacheRequest[reqKey];
console.log("abortCacheRequest", cacheRequest);
}
}
- 在请求发送前,检查
config
中是否包含isAbort
字段,如果为true
,则取消之前的相同请求。 - 使用
AbortController
来取消请求,并将signal
属性添加到config
中。
javascript
service.interceptors.request.use(
(config) => {
const { url, method, isAbort = false } = config;
if (isAbort) {
const reqKey = `${url}&${method}`;
abortCacheRequest(reqKey);
const controller = new AbortController();
config.signal = controller.signal;
cacheRequest[reqKey] = controller;
}
return config;
},
(error) => {
console.log(error); // for debug
return Promise.reject(error);
}
);
- 在响应返回后,如果请求被取消,则从缓存中删除对应的请求。
- 处理响应错误,如果请求被取消,则返回自定义的错误信息;否则,显示错误消息并返回错误对象。
javascript
service.interceptors.response.use(
(response) => {
const { url, method, isAbort = false } = response.config;
if (isAbort) delete cacheRequest[`${url}&${method}`];
const res = response.data;
return res;
},
(error) => {
if (isCancel(error)) {
return Promise.reject({
message: "重复请求,已取消",
});
}
console.log("err" + error); // for debug
Message({
message: "登录连接超时(后台不能连接,请联系系统管理员)",
type: "error",
duration: 5 * 1000,
});
error.data = { msg: "系统内部错误,请联系管理员维护" };
return Promise.reject(error);
}
);
完整代码如下:
javascript
import axios from "axios";
const service = axios.create({
baseURL: "http://localhost:3001", // api 的 base_url
timeout: 500000, // request timeout
});
// isAbort Start
const { isCancel } = axios;
const cacheRequest = {};
// 删除缓存队列中的请求
function abortCacheRequest(reqKey) {
if (cacheRequest[reqKey]) {
// 通过AbortController实例上的abort来进行请求的取消
console.log("abortCacheRequest", reqKey);
cacheRequest[reqKey].abort();
delete cacheRequest[reqKey];
console.log("abortCacheRequest", cacheRequest);
}
}
// isAbort End
// request interceptor
service.interceptors.request.use(
(config) => {
// isAbort Start
const { url, method, isAbort = false } = config;
if (isAbort) {
// 请求地址和请求方式组成唯一标识,将这个标识作为取消函数的key,保存到请求队列中
const reqKey = `${url}&${method}`;
// 如果config传了需要清除重复请求的isAbort,则如果存在重复请求,删除之前的请求
abortCacheRequest(reqKey);
// 将请求加入请求队列,通过AbortController来进行手动取消
const controller = new AbortController();
config.signal = controller.signal;
cacheRequest[reqKey] = controller;
}
// isAbort End
return config;
},
(error) => {
// Do something with request error
console.log(error); // for debug
Promise.reject(error);
}
);
// response interceptor
service.interceptors.response.use(
(response) => {
// isAbort Start
const { url, method, isAbort = false } = response.config;
if (isAbort) delete cacheRequest[`${url}&${method}`];
// isAbort End
const res = response.data;
return res;
},
(error) => {
if (isCancel(error)) {
// 通过AbortController取消的请求不做任何处理
return Promise.reject({
message: "重复请求,已取消",
});
}
console.log("err" + error); // for debug
Message({
message: "登录连接超时(后台不能连接,请联系系统管理员)",
type: "error",
duration: 5 * 1000,
});
error.data = { msg: "系统内部错误,请联系管理员维护" };
return Promise.reject(error);
}
);
export default service;
调用例子
javascript
export function getWetherByTwoExample() {
return service({
url: '/data',
method: 'get',
isAbort: true
})
}
项目demo地址:cancelRequest: 避免重复调用接口的demo