注册
web

一文学会请求中断、请求重发、请求排队、请求并发

大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处!

以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。

阅读下文需要了解前置知识:promise、class、axios


请求中断


1.判定相同请求:请求url、请求方法、请求params参数、请求 body参数,四个值都相同,则认为是一个相同的请求。

2.判断请求中断:在上一个相同请求还没有得到响应前,再次请求,则会自动中断。


image.png


请求重发(无感刷新token)


1.当前请求返回401时,执行刷新token。

2.当同时存在多个请求返回401时,可在类中维护一个静态变量保存请求刷新接口的promise,防止多次调用刷新token。

3.RetryRequest类实例化参数:

   instance:请求实例对象

   success:刷新成功回调函数

   error:刷新失败回调函数

image.png


请求排队



  1. queue:请求等待队列。

  2. isWating:是否正在等待上个请求响应。

  3. add:向队列里加入一个等待请求的promise的resolve方法,执行该方法可立刻发送下一个请求 。

  4. next:执行下一个请求方法,在上一个请求响应后调用。

    image.png

响应处理


image.png


axios请求实例


image.png


测试


1.请求中断测试


快速点击test请求按钮多次


image.png


2.刷新token请求重发测试


(1)当用户没有登录请求接口时


image.png


(2)当用户登录后,accessToken过期,但refreshToken还没过期调用接口时


image.png
在调用刷新token接口成功后,将重发失败的test接口


(3)当refreshToken过期后调用接口时


image.png
这时已经无法刷新token了,只能乖乖跳转到登录页面了。


3.请求排队测试


(1)没有使用请求排队时


场景:当输入框输入关键字实时查找内容时,由于网络原因,可能会出现先请求的后响应的请求,导致请求错乱。

如下,模拟网络请求延迟:
image.png


当输入框依次输入1、2、3、4、5时,期望的返回结果应该是1,12,123,1234,12345。
但确得到了以下的结果:
image.png


(2)使用请求队列时


在网络请求的waterfall列可以清晰看到,当上一请求完成才会执行下一请求,直到等待队列执行完成。
image.png


源码


后端接口


var express = require('express');
var router = express.Router();

const access_token = 'access_token'
const refresh_token = 'refresh_token'
// token有效期(单位毫秒)
const tokenValidTime = 1000*2
// 刷新token有效期
const refreshTokenValidTime = 1000*5
// 登录时间,模拟token过期
let loginTime;
// 模拟判断token是否过期
const IsTokenExpired = () => {
if(new Date().getTime() > loginTime + tokenValidTime) {
return true
}
return false
}
router.post('/login', (req, res) => {
loginTime = new Date().getTime()
res.json({
access_token,
refresh_token
})
})

router.post('/refresh-token', (req, res, next) => {
const refreshToken = req.headers.authorization
console.log('refresh-token', refreshToken)
if(refreshToken !== refresh_token) {
return res.status(401).json({
msg: ' refreshToken不正确!'
})
}
if(new Date().getTime() > loginTime + refreshTokenValidTime) {
return res.status(401).json({
msg: ' refreshToken已过期,请重新登录!'
})
}
loginTime = new Date().getTime()
res.json({
access_token: 'access_token',
refresh_token: 'refresh_token'
})
})

router.get('/test', (req, res, next) => {
const token = req.headers.authorization
if(token !== access_token) {
return res.status(401).json({
msg: '没有访问权限'
})
}
if(IsTokenExpired()) {
return res.status(401).json({
msg: 'token已过期'
})
}
res.json({
name: '哈哈'
})
})

router.get('/random', (req, res) => {
const keyword = req.query.keyword
setTimeout(() => {
res.json({
value: keyword
})
// 5秒内随机返回,测试网络请求延迟效果
}, Math.random()*5000);
})

module.exports = router;

前端


Index.js


import axios from "axios"
import AbortRequest from './hooks/AbortRequest'
import ResponseHanlder from "./hooks/ResponseHanlder"
import { getAccessToken, getRefreshToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'
import { useRequestKey } from "./hooks/useRequestKey"
import RequestQueue from "./hooks/RequestQueue"
import { isAddQueue } from '@/api'

export const baseURL = '/api'
const timeout = 6000

// 创建axios实例
const instance = axios.create({
baseURL,
timeout
});

// 创建中断请求控制器
const abortRequest = new AbortRequest()
// 创建响应处理器
const responseHandler = new ResponseHanlder(instance)
// 创建请求队列排队实例
const requestQueue = new RequestQueue()

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
console.log('在发送请求之前做些什么', config)
// 在发送请求之前做些什么
if(config.url !== '/login') {
const token = config.url === refreshTokenUrl ? getRefreshToken() : getAccessToken()
config.headers.Authorization = token
}
// 刷新token接口不用创建取消请求,已经再RetryRequest类维护静态属性
if(config.url !== refreshTokenUrl) {
abortRequest.create(useRequestKey(config), config)
}
// 加入请求等待队列
if(isAddQueue(config)) {
return requestQueue.add(config.url, config)
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
const config = response.config
console.log('响应成功', response)
abortRequest.remove(useRequestKey(config))
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return responseHandler.success(response)
}, function (error) {
const config = error.config
console.log('响应错误', config)
if(config) {
abortRequest.remove(useRequestKey(config))
}
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if(axios.isCancel(error)) {
return Promise.reject('已取消重复请求!')
}
return responseHandler.error(error)
});

export default instance;

AbortRequest.js


// 重复请求中断类
class AbortRequest {
constructor() {
// 请求中断控制器集合
this.list = new Map()
}
// 创建中断请求控制器
create(key, config) {
const controller = new AbortController();
config.signal = controller.signal
// 集合中存在当前一样的请求,直接中断
if(this.list.has(key)) {
controller.abort()
} else {
this.list.set(key, controller)
}
}
// 请求完成后移除集合中的请求
remove(key) {
this.list.delete(key)
}
}
export default AbortRequest

RequestQueue.js


/**
* 相同url请求队列,排队执行维护类
*/

class RequestQueue {
constructor() {
// 请求等待队列
this.queue = {}
// 正在等待上一请求执行中
this.isWating = false
}
add(url, config) {
return new Promise((resolve) => {
const list = this.queue[url] || []
if(this.isWating) {
// 当前请求url存在等待发送的请求,则放入请求队列
list.push({ resolve: () => resolve(config) })
} else {
// 没有等待请求,直接发送
resolve(config)
this.isWating = true
}
this.queue[url] = list
console.log('list', list)
})
}
// 响应处理
next(url) {
this.isWating = false
// 拿出当前请求url的下一个请求对象
if(this.queue[url]?.length > 0) {
const nextRequest = this.queue[url].shift()
// 执行请求
nextRequest.resolve()
}
}
}

export default RequestQueue

RetryRequest.js


/**
* 无感刷新token类
*/

class RetryRequest {
// 解决存在多个并发请求时,重复调用刷新token接口问题
static refreshTokenPromise = null
constructor({
instance, // axios实例
success, // 刷新token成功执行的回调函数
error // 刷新token失败执行的回调函数
}
) {
this.instance = instance
this.success = success
this.error = error
}
/**
* @param config 当前请求对象,等待token刷新完成再重复执行
* @param refreshTokenApi 刷新token方法
*/

useRefreshToken(config, refreshTokenApi) {
if(!config.headers.Authorization) {
this.error()
return Promise.reject('token不存在!')
}
return new Promise((resolve, reject) => {
if(!RetryRequest.refreshTokenPromise) {
// refreshTokenPromise不为null,则当前正在执行刷新token方法,不再重复调用
RetryRequest.refreshTokenPromise = refreshTokenApi()
}
RetryRequest.refreshTokenPromise.then(res => {
// 刷新token成功
this.success(res)
// 重新发送请求
this.instance(config).then(data => {
resolve(data)
}).catch(err => {
// 重发失败
reject(err)
})
}).catch(err => {
// refreshToken失效或刷新token失败
this.error()
reject(err)
}).finally(() => {
// 刷新token调用完成,重置
RetryRequest.refreshTokenPromise = null
})
})
}
}

export default RetryRequest

ResponseHanlder.js


import RetryRequest from './RetryRequest'
import { refreshToken as refreshTokenApi } from '@/api/index'
import { getRefreshToken, setRefreshToken, setAccessToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'

/**
* 响应处理类
*/

class ResponseHanlder {
constructor(instance) {
this.retryRequest = new RetryRequest({
instance,
success: (res) => {
const { access_token, refresh_token } = res
setAccessToken(access_token)
setRefreshToken(refresh_token)
},
error: () => {
console.log('刷新token失败!')
// 执行失败逻辑...
}
})
}
// 请求正常响应方法
success(response) {
// 对响应数据做处理
return response.data
}
// 请求错误响应方法
error(error) {
const status = error.response?.status
// 当前返回401,且不是调用刷新token接口响应的(避免后端刷新token失败返回401导致死循环的情况)
if(status === 401 && error.config.url !== refreshTokenUrl) {
return this.retryRequest.useRefreshToken(
error.config,
() => refreshTokenApi(getRefreshToken())
)
} else {
return Promise.reject(error.response)
}
}
}

export default ResponseHanlder

request.js


import instance from './index'

class Request {
constructor() {

}
get(url, params, args) {
return instance.get(url, {
params,
...args
})
}
delete(url, params) {
return instance.get(url, {
params
})
}
post(url, data) {
return instance.post(url, data)
}
put(url, data) {
return instance.put(url, data)
}
}

export default new Request();

结语


还有一个控制请求并发数量还没进行扩展,相信大家了解了请求排队的思路后,实现请求并发控制也不是什么难事了。



无感刷新token参考文章:juejin.cn/post/728974…



作者:云上客人
来源:juejin.cn/post/7293806405650464808

0 个评论

要回复文章请先登录注册