合并重复请求

在工作中遇到一种情况,前端的一些请求需要携带 token 完成认证,而这个 token 在一段时间内是保持不变的,所以最佳的方式应该是请求一个 token 之后将其存储起来,在未过期的时候可以重复使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
let token
async getToken() {
if (!token || Date.now() - token.timestamp > 3600 * 1000) {
const res = await axios.get('/token')
token = res.data
}
return token
}

async function requestWithToken() {
const token = await getToken()
return await axios(arguments)
}

在一个页面中如果有多个请求时,getToken 方法会首先查询是否有未过期的 token ,否则就会从后端获取,这样就避免了浪费。


然而在页面加载时短时间内多次调用 requestWithToken 方法时,第一次的请求还未返回,token 对象依旧为空,所以之后依旧会发送调用 getToken 方法。

我首先想到的是设置一个 pending 状态表示 token 正在获取中

1
2
3
4
5
6
7
8
9
10
11
let token
let pending = false
async function getToken () {
pending = true
if (!token || Date.now() - token.timestamp > 3600 * 1000 || !pending) {
const res = await axios.get('/token')
pending = false
token = res.data
}
return token
}

但是这样做的话虽然避免了重复发送请,但是获取返回的值却有可能是空的 token ,所以我们还需要额外的工作,需要请求 token 的时候先查询是否存在正在发送的请求,如果是则将 promise 的回调函数存起来,在第一次请求的回调函数中统一进行处理。

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
let token
async function getToken () {
if (!token || Date.now() - token.timestamp > 3600 * 1000) {
token = await requestPool('/token')
}
return token
}

const pool = {}
function requestPool(url) {
if (!pool[url]) {
pool[url] = []
return new Promise((resolve, reject) => {
axios.get(url)
.then(res => {
resolve(res)
pool[url].forEach(cb => cb.resolve(res))
})
.cath(e => {
reject(e)
pool[url].forEach(cb => cb.reject(e))
})
.finally(() => {
Reflect.delete(pool, url)
})
})
}

return new Promise((resolve, reject) => {
queue.push({resolve, reject})
})
}

ps:浏览器本身自带缓存 get 请求的功能,但是可能受业务限制,存在接口 http headers 无法正确设置的情况,所以还需要前端做些额外的工作来进行缓存。利用这种方法甚至还可以缓存 post 请求。