实现 Axios 同步请求:兼容 XHR 的队列式解决方案

在我们开发 vue 应用项目时,axios 是一个非常好用的 HTTP 请求库,我们可以用它来发送 HTTP 请求,但是在某些情况下,我们需要将使用 axios 的所有 HTTP 请求变为同步请求,例如在适配某些项目时,后端接口是对 token 进行单线加密验证的场景。

为了 api 库能够在多个模块中使用,便于统一修改,我们可以封装一下 axios 的请求。

在适配某些其他项目时,其他项目也有可能发送 XHR 请求,但我们又无法管控和修改其他项目的代码,所以我们要获取当前是否有 XHR 请求,如果有,则需要排队等待上一个请求响应结束后才会去发送下一个请求。

这里我们需要一个 XHR 检测工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// axios/xhr-detect.js
// 避免重复处理
if (!window.XMLHttpRequest.__isPatched) {
const OriginalXHR = window.XMLHttpRequest;

if (!Object.prototype.hasOwnProperty.call(window, "WA_activeXHRCount")) {
window.WA_activeXHRCount = 0;
}

// 重写XMLHttpRequest构造函数
window.XMLHttpRequest = function () {
const xhr = new OriginalXHR();

// 保存原生send方法
const originalSend = xhr.send.bind(xhr);

// 重写send方法以计数
xhr.send = function (...args) {
window.WA_activeXHRCount++;
originalSend(...args);
};

// 请求结束时减少计数器
const decrement = () => {
window.WA_activeXHRCount--;
};

// 监听所有结束事件
xhr.addEventListener("load", decrement);
xhr.addEventListener("error", decrement);
xhr.addEventListener("abort", decrement);
xhr.addEventListener("timeout", decrement);

return xhr;
};

// 标记已修补,避免重复
window.XMLHttpRequest.__isPatched = true;
}

// 提供检查方法
window.getActiveXHRCount = () => window.WA_activeXHRCount;

接下来我们要封装 axios 请求,达到所有使用该库发送请求的 api 请求都变成同步请求的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// axios/index.js
import axios from "axios";
import api from "../utils/api";
import "./xhr-detect";

// 是否是{}样的对象
const isObject = (obj) => {
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
};

// 检测函数是否在api对外暴露的方法集合中
const isFunIn = (fun) => {
const name = fun.name;
return Object.keys(api).indexOf(name) > -1;
};

let defaultConfig = {
baseURL: "/" + process.env.VUE_APP_API_PRE,
timeout: 1000 * 15,
// 跨域请求时携带cookie凭证
withCredentials: true,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Requested-With": "XMLHttpRequest",
},
};
const service = axios.create();

// 定义公共变量
if (!Object.prototype.hasOwnProperty.call(window, "WA_requestQueue")) {
window.WA_requestQueue = [];
}
if (!Object.prototype.hasOwnProperty.call(window, "WA_requestQueue")) {
window.WA_isRequesting = false;
}

// 队列处理
const processQueue = () => {
// 在当前有axios在请求中时、请求队列没有数据了,都暂不处理
if (window.WA_isRequesting || window.WA_requestQueue.length === 0) {
return;
}

// 如果平台有未完成的xhr请求时,延迟处理
if (window.getActiveXHRCount() > 0) {
setTimeout(() => {
processQueue();
});
return;
}

window.WA_isRequesting = true;
const { config, fun, resolve, reject } = window.WA_requestQueue.shift();

let options;
if (isObject(config) && fun === null) {
// 传递的纯axios的配置对象
options = { ...config };
} else if (Array.isArray(config) && typeof fun === "function" && isFunIn(fun)) {
// 传递的api处理函数,在队列中实时计算数据
options = fun(...config);
} else {
console.error("request参数错误,请检查api配置");
return;
}

options = Object.assign(defaultConfig, options);

service(options)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
})
.finally(() => {
window.WA_isRequesting = false;
processQueue();
});
};

/**
* 请求处理方法
* @param {*} config 可以是axios的对象配置,也可以是api对外暴露的函数参数值数组按顺序传递
* @param {*} fun api对象暴露的函数
* @returns
*/
const request = (config = {}, fun = null) => {
return new Promise((resolve, reject) => {
window.WA_requestQueue.push({ config, fun, resolve, reject });
processQueue();
});
};

// 设置默认的请求配置
const setDefaultOption = (option) => {
defaultConfig = Object.assign(defaultConfig, option);
};

export default { request: request, service: service, setDefaultOption: setDefaultOption };

提示
上面的有个 api 库,是对外暴露了一些处理方法,因为后端特定设计的原因,需要我们对传递的数据进行动态加密,而且不同的相请求,处理方法不一样,例如文件上传、命令调用等,需要有不同的处理方法,所以,我们把处理方法封装到 api 中,然后通过 api 暴露给外部使用,这样,我们的代码就清晰了很多。所以 api 一个函数集合,在上面的代码中也有用于判断函数是否在预计中的逻辑。

我们需要将该模块库中的所有的处理方法及 axios 库一起对外暴露

1
2
3
4
import api from "./utils/api";
import axios from "./axios/index";

export default { api: api, axios: axios };

将上面的库构建后,可以在具体的应用项目中进行使用。

这样我们就实现了将 axios 的所有请求变为同步请求,而且不影响 axios 的请求与响应的拦截器的定义,在定义 api 中可以像下面这样使用。

首先,在我们的项目中需要定义一个统一的 api 处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// utils/request.js

// 引入前面推送的库
import api from "@webape/api";

// 创建axios实例
const service = api.axios.service;

api.axios.setDefaultOption({
baseURL: "/" + process.env.VUE_APP_API_PRE,
timeout: 10000,
// 跨域请求时携带cookie凭证
withCredentials: true,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Requested-With": "XMLHttpRequest",
},
});

// request拦截器
service.interceptors.request.use(
(config) => {
//...
return config;
},
(error) => {
//...
return Promise.reject(error);
}
);

// response拦截器
service.interceptors.response.use(
(response) => {
const { data } = response;
if (Object.prototype.toString.call(data) === "[object Array]") {
return Promise.resolve(data);
} else {
return Promise.reject("数据结构异常");
}
},
(error) => {
// 判断请求异常信息中是否含有超时timeout字符串
if (error.message.includes("timeout")) {
return Promise.reject("请求超时,请稍后再试");
} else if (error.message.includes("NetworkError")) {
return Promise.reject("网络错误,请稍后再试");
} else {
return Promise.reject(error);
}
}
);

export default api.axios.request;

然后在应用项目的 api 定义文件中像下面这样使用:

1
2
3
4
5
6
7
8
9
10
11
import request from "@/utils/request";
import api from "@webape/api";

// 使用函数处理方式
export function show(data) {
return request(["commandXml函数参数param1", "commandXml函数参数param2", "commandXml函数参数param3"], api.api.commandXml);
}
// 直接使用axios的请求参数
export function show2(data) {
return request({ url: "/webape.net/api/commandXml", method: "post", data: { webape: "webape" } });
}

然后就可以在具体的 vue 文件中 import 后进行使用了,这样只要使用封装的 axios 库的请求,都会被处理为同步请求,而且兼容在同一个项目中,有其他不被管控制的 XHR 请求,也会等待串行处理。