需求
在并发接口请求的时候,能够自动对失败的请求进行重发尝试(超过指定重试次数则不再重试),并将最终的结果返回(包含每个请求是否成功、返回结果)
核心思路
代码实现
使用案例
为了演示我们代码的最终实现效果,我们使用如下的案例:
js
function request1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('请求完毕111');
resolve(111)
}, 1000);
})
}
function request2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('请求完毕222');
reject(222)
}, 2000);
})
}
function request3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('请求完毕333');
reject(333)
}, 3000)
})
}
如上存在3个异步请求,我们想要并发发送,并获取到它们的请求结果,理想的方式是这样的:
js
new RequestParallelWithRetry([ request1, request2, request3 ]).send().then((value) => {
console.log(value);
})
因此,这里我们需要实现一个RequestParallelWithRetry
类
传入方法标记
根据我们上面提出的思路,我们需要将传入的请求方法进行标记并保存它们的关系,便于后续请求失败时可以利用这个标记来再次调用这个方法
js
class RequestParallelWithRetry {
requestList
retryCount
map = {}
result = []
constructor(requestList = [], retryCount = 3) {
this.requestList = requestList
this.retryCount = retryCount
// 需要给每个方法添加标记
this.requestList.forEach((item, index) => {
item._index = index
this.map[ index ] = item
})
console.log(this.map);
}
}
这里我们使用传入的请求列表下标作为标记,并保存起来,打印出来的map
结果如下:
并发请求发送
之后我们需要实现RequestParallelWithRetry
类的send
方法,它用来并发发送请求,并且要求能够得知每个请求的执行结果。因为我们这里请求的返回结果都是一个promise
对象,因此我们可以利用Promise.allSettled
方法来获取所有promise
的最终结果:
js
send() {
const queue = []
this.requestList.forEach(request => {
queue.push(request())
})
Promise.allSettled(queue).then((value) => {
console.log('所有请求都完成了', value)
})
}
此时,调用new RequestParallelWithRetry([ request1, request2, request3 ]).send()
将得到如下的打印结果:
可以看到:
request2
和request3
都请求失败了(返回rejected
)- 我们此时需要做的其实是将这些所有状态为
rejected
的请求再次执行直到所有的请求都成功(fulfilled)
了(先不考虑重试次数限制的情况) - 因此,我们需要递归执行这个过程(发送请求->找到失败的请求->再次发送)
包装请求的执行结果
我们已经知道了我们重复执行那些失败的请求直到成功,但是还存在一个问题:现在我们只能知道失败请求的结果,但是我们无法根据这个结果来找到原先执行请求的方法。因此,我们需要对传入的请求进行一层包装,使得执行的返回结果中包含一个标记,我们能根据这个标记找到这个请求方法:
js
send() {
const queue = []
this.requestList.forEach(request => {
queue.push(this._wrapRequest(request))
})
Promise.allSettled(queue).then((value) => {
console.log('所有请求都完成了', value)
})
}
_wrapRequest(request) {
return new Promise((resolve, reject) => {
request().then((value) => {
resolve({
value,
index: request._index
})
}).catch(reason => {
reject({
reason,
index: request._index
})
})
})
}
此时,我们就能在请求结果中获取到原先请求方法的标记index
了,利用这个标记和已经保存过的map
关系映射,就能获取到原先的请求方法,从而再次调用
请求失败再次调用
js
send() {
let queue = []
this.requestList.forEach(request => {
queue.push(this._wrapRequest(request))
})
const sendAllSettled = () => {
Promise.allSettled(queue).then((value) => {
queue = []
console.log('所有请求都完成了', value);
// 检查是否存在失败的请求
value.forEach(result => {
if (result.status === "rejected") {
const { reason, index } = result.reason
const request = this.map[ index ]
queue.push(this._wrapRequest(request))
}
})
// 检查队列中是否还需要发送请求
if (queue.length) {
sendAllSettled()
}
})
}
sendAllSettled()
}
这里,我们在send
方法内部递归调用sendAllSettled
方法,sendAllSettled
方法做的事情很简单:
- 将
queue
队列中的请求发送出去 - 将失败的请求重新添加到队列中,再次发送
重试次数限制
在上面的代码中,我们已经实现了并发请求失败时自动重新尝试,但是在实际开发中,我们并不能让它无限制地重试下去,而是应该指定重试的次数,一旦超过这个次数了,就不再重试
具体的实现也很简单,只需要给每个请求方法添加一个记录重试次数的属性_retryCount
即可,一旦这个_retryCount
超过retryCount
就不再重试了
具体代码如下:
js
send() {
let queue = []
this.requestList.forEach(request => {
request._retryCount = 0
queue.push(this._wrapRequest(request))
})
const sendAllSettled = () => {
Promise.allSettled(queue).then((value) => {
queue = []
console.log('所有请求都完成了', value);
// 检查是否存在失败的请求
value.forEach(result => {
if (result.status === "rejected") {
const { reason, index } = result.reason
const request = this.map[ index ]
if (request._retryCount < this.retryCount) {
queue.push(this._wrapRequest(request))
request._retryCount++
}
}
})
// 检查队列中是否还需要发送请求
if (queue.length) {
sendAllSettled()
}
})
}
sendAllSettled()
}
并发请求执行结果返回
上面的代码中,我们已经实现了并发请求的失败重试以及重试次数的限制了,最后我们需要将并发请求的最终执行结果返回给用户
由于并发请求是一系列的异步操作,因此我们对于send
方法的返回结果也应该是一个promise
对象,这个对象包含了每个请求的执行结果(成功或失败)和返回结果
具体实现如下:
js
send() {
return new Promise((resolve) => {
// 1. 将所有的网络请求进行包装,使得其返回结果中携带有_index
// 2. 发送所有的网络请求
let queue = []
this.requestList.forEach(request => {
// 3. 记录每个请求的重试次数
request._retryCount = 0
queue.push(this._wrapRequest(request))
})
const sendAllSettled = () => {
Promise.allSettled(queue).then((value) => {
// 3. 清除发送队列
queue = []
console.log('所有请求都完成了', value);
// 4. 检查是否存在失败的请求
value.forEach(result => {
if (result.status === "fulfilled") {
const { value, index } = result.value
this.result[ index ] = { value, status: 'fulfilled' }
}
else if (result.status === "rejected") {
const { reason, index } = result.reason
const request = this.map[ index ]
if (request._retryCount < this.retryCount) {
queue.push(this._wrapRequest(request))
request._retryCount++
} else {
this.result[ index ] = { reason, status: 'rejected' }
}
}
})
// 5. 检查队列中是否还需要发送请求
if (queue.length) {
sendAllSettled()
} else {
resolve(this.result)
}
})
}
sendAllSettled()
})
}
最终用户的调用形式和执行结果会是这样的:
js
new RequestParallelWithRetry([ request1, request2, request3 ]).send().then((value) => {
console.log(value);
})