前言
以下收集了我工作以来遇到的对接口行为控制的操作方案,有一些暂时记不得了,等以后想到了再做补充。
轮番查询
一般轮番查询分情况看,如果是那种返回的数据大致只有AB状态的,且短时间内不会突然改变的。我们就做普通的轮询就好了。
有这样一种情况,接口请求间隔时间不短,并且可能每次请求到的数据都是不一样的。要怎么保证页面上渲染到的是目前最新的数据呢?
如果接口的请求是时间是保证的,那么无需任何操作,但事实往往不是这样的,例如第一次请求晚与最后一次请求,结果页面就展示成第一次请求的数据了。
我自己总结了几个方法
前后端统一标识
就是前端自己维护一个全局计数器变量count,每次请求的时候就加1,并且作为入参返回给后端。后端每次请求的时候也把这个count返回。然后前端每次接口拿到结果的时候都把这个返参count和全局的count对比,一样才展示数据。
这么简单就不用贴代码了吧哈哈,这种方法稍微麻烦就是每个需要这样轮番处理的接口都要和后端沟通一下。
控制重复请求
就是判断当借口还在pedding的时候,就不再触发新的请求。
可以在axios的封装文件里添加这个功能,大概实现方式就是,用一个数组变量存放正在请求的接口地址,当a请求触发时,把地址a推入数组,如果a请求完毕,就把数组里的a地址删除。当a再次触发时,判断此时a请求地址是否在数组里,如果在就不继续请求。
具体封装细节可以看我这篇文章:【场景方案】如何去设计并二次封装一个好用的axios,给你提供一个另类写法,另加一些思考
最后一个之前的请求取消
这是我认为最好的方式,放在下面记录了
并发请求
一般情况下其实用Promise.all的方式就能解决需求了,但是如果你想做的更好一些,还是要重新封装一下。
其实原理就是我这篇文章里讲的【并发请求一定数量的接口】部分。
// 模拟100个异步请求
const arr = [];
for (let i = 0; i < 100; i++) {
arr.push(() => new Promise((resolve) => {
setTimeout(() => {
console.log('done', i);
resolve();
}, 100 * i);
}));
};
const parallelRun = () => {
const runingTask = new Map(); // 记录正在发送的异步请求(闭包存储)
const inqueue = (totalTask, max) => { // 异步请求队列,每组请求的最大数量
// 当正在请求的任务数量小于每组请求的最大数量,并且还有任务未发起时,就推入请求
while (runingTask.size < max && totalTask.length) {
const newTask = totalTask.shift(); // 弹出新任务
const tempName = totalTask.length; // 以长度命名?
runingTask.set(tempName, newTask);
newTask().finally(() => {
runingTask.delete(tempName);
inqueue(totalTask, max); // 每次一个任务完成后就继续塞入新任务
});
}
}
return inqueue;
};
parallelRun()(arr, 6);
但是这个方法建议封装在统一的axios请求方法里。
其实很容易实现的,在【场景方案】如何去设计并二次封装一个好用的axios,给你提供一个另类写法,另加一些思考,里面的三次封装函数里加个map类型变量,以地址为键,值为一个对象,属性分别是最大请求量、当前正在请求的任务,以及还剩余的任务。
具体就先暂时不写了哈,等以后有空了在补充在文章里。
服务端通知
其实就是后端主动推送功能。
后端返回标识
其实实现起来和token失效一样,咱们一般如果token失效,后端不是就会返回401吗。如果其他普通的接口,也可以这样处理。
例如要主动通知前端什么事情,可以类似的反馈给前端:
- 返回个约定的错误码。例如在大对象里面的某个字段返回。
- 或者通过响应头返回给前端
这个方案唯一的缺陷就是,需要前端主动发起一次请求。
websock推送
这玩意说实话是最好的解决方案了,但是前端和后端的改造成本大,一般公司也不会这样出方案。
不过说实话这个东西一旦搭建起来了,很多需要主动推送的需求用这个完美解决。
SSE推送
这个我是看GPT使用的,以后有时间自己试试,可以先看看这篇文章的讲解:一文读懂即时更新方案:SSE
token无感刷新
大概就是之前使用的token我们称之为短期token,然后每次登录后端多返回一个长期token。
当短期token失效后,后端普通接口返回401,前端判断后请求一个更新短期token的接口,请求的时候请求头里的token要换成长期token,后端返回新的短期token。
最后如果连长期token都失效了,那就要重新登录。
以下是我的模拟代码,大家可以复制粘贴研究下,还没用在实战过,可能还有些需要完善的地方。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
</style>
</head>
<body>
<button id="reLogin">重新登录</button>
<button id="request1">正常请求</button>
<button id="request2">开启无感token刷新机制</button>
<button id="request3">关闭无感token刷新机制</button>
<button id="removeLongToken">让长token失效</button>
<div>
<p id="tokenP"></p>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
<script>
// 请求token刷新接口
let promise
function requestToken() {
loginTimeStart = Date.now() // 可忽略不看
// 把请求包装成一个promise,防止token刷新接口短期内被重复请求
if (promise) {
console.log('r', promise);
return promise
}
promise = new Promise(async (resolve) => {
let res = await request({
url: isLongTokenOverTime ? 'refresh1' : 'refresh',
method: 'post',
headers: {
Authorization: `Bearer ${getLongToken()}` // 带的是长token
},
isRefreshTokenRequest: true // 标记这是一个刷新token的请求
})
resolve(res)
})
promise.finally(() => {
promise = null
})
if (isLongTokenOverTime) isLongTokenOverTime = false // 可忽略不看
return promise
}
let request = axios.create({
//1,基础配置
baseURL: 'https://mock.mengxuegu.com/mock/64ef0df9e70b8004a69e98cd/token',
timeout: 5000, // 设置超时时间为5s
// headers: {
// Authorization: `Bearer ${getToken()}` // 不建议初始化就写入,加入无感知token刷新后,每次都要更新下
// }
})
// 设置请求拦截器
request.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config; // 必须返回配置
}, error => {
return Promise.reject(new Error(error))
})
// 响应拦截器(处理返回的数据)
request.interceptors.response.use(async (res) => {
//响应统一处理
let { code, message, data } = res.data;
// 一般token会根据响应头返回,但我们接口用的easymock无法改写响应头,用data处理代替
let responseHeader = data // 假设这个就是响应头
// 短期token设置
if (responseHeader.authorization) {
// request.defaults.headers.Authorization = `Bearer ${responseHeader.authorization}` 直接在请求拦截器中写
setToken(responseHeader.authorization)
}
// 长期token存储
if (responseHeader.refreshToken) {
setLongToken(responseHeader.refreshToken)
}
if (code === 401 && res.config.isRefreshTokenRequest !== true) {
// 有无感知token刷新机制走这里
if (isRefreshTokenOpen) {
await requestToken()
// request.defaults.headers.Authorization = `Bearer ${getToken()}` 直接在请求拦截器中写
// 更新了短token后,重新请求 新请求有错误咋办
// request.request(res.config) // 实际上代码应该走这里的
request.request({
url: 'list',
method: 'get'
}) // 为了演示模拟请求正常的接口
} else {
alert(message)
return Promise.reject(new Error(message)); // 抛出可以自定义错误提示
}
} else if (code === 401 && res.config.isRefreshTokenRequest === true) {
// 如果刷新请求也401了,说明长token也过期了,要重新登录了
alert(message)
return Promise.reject(new Error(message)); // 抛出可以自定义错误提示
}
if (code !== 200) {
return Promise.reject(new Error(message));
}
console.log('正常拿到数据', res.data);
tokenPDom.textContent = getToken() // 显示在页面上
return res.data;
}, error => {
// 例如断网、跨域、状态码问题的报错
// error.response.status 这里可以拿到接口真正的状态码,还是要看后端是怎么设计接口的
return Promise.reject(new Error(error));
})
// 以下代码不用看,都是用来模拟的-------------------------------------------------------------
// 获取短token
function getToken() {
return localStorage.getItem("token"); // 忽略token加密解密了
}
function setToken(data) {
localStorage.setItem("token", data); // 忽略token加密解密了
}
// 获取长token
function getLongToken() {
return localStorage.getItem("longToken"); // 忽略token加密解密了
}
function setLongToken(data) {
localStorage.setItem("longToken", data); // 忽略token加密解密了
}
let request1Dom = request1
let request2Dom = request2
let request3Dom = request3
let reLoginDom = reLogin
let tokenPDom = tokenP
let removeLongTokenDom = removeLongToken
let isRefreshTokenOpen = false // 是否开启无感刷新token
let isLongTokenOverTime = false // 长token是否过期
// 模拟token过期
let loginTimeStart = 0
request1Dom.onclick = function () {
let nowDate = Date.now()
// 假设5s后token过期,请求另外一个接口返回401
if (nowDate - loginTimeStart > 5000) {
request({
url: 'list1',
method: 'get'
})
} else {
request({
url: 'list',
method: 'get'
})
}
}
request2Dom.onclick = function () {
isRefreshTokenOpen = true
}
request3Dom.onclick = function () {
isRefreshTokenOpen = false
}
reLoginDom.onclick = function () {
loginTimeStart = Date.now()
request({
url: 'login',
method: 'post'
})
}
removeLongTokenDom.onclick = function () {
isLongTokenOverTime = true
}
</script>
</body>
</html>
请求取消
例如同一个请求连续发送多个,要保证只拿取最后一次请求的数据,可以试试这个方法。
把最后一个发出的请求之前的所有请求都取消,用的是axios.CancelToken
,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
</style>
</head>
<body>
<button id="request1">有接口取消机制</button>
<button id="request2">无接口取消机制</button>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.6/axios.min.js"></script>
<script>
let request = axios.create({
//1,基础配置
baseURL: 'https://mock.mengxuegu.com/mock/64ef0df9e70b8004a69e98cd/token',
timeout: 5000, //
})
// 存放请求取消的相关对象
let CancelToken = null
let source = null
// 设置请求拦截器
request.interceptors.request.use((config) => {
const { requestCancelOpen } = config
if (CancelToken && requestCancelOpen) { // 如果存在上一个接口CancelToken对象,并且开启取消请求机制
source.cancel('请求取消'); //主动取消,传递具体信息
}
if (requestCancelOpen) {
CancelToken = axios.CancelToken; // 创建最新的对象
source = CancelToken.source();
config.cancelToken = source.token // 关键,给真正的cancelToken配置赋值
}
return config
}, error => {
return Promise.reject(new Error(error))
})
// 响应拦截器(处理返回的数据)
request.interceptors.response.use(async (res) => {
//响应统一处理
let { code, message, data } = res.data;
if (code === 401) {
alert(message)
return Promise.reject(new Error(message)); // 抛出可以自定义错误提示
}
if (code !== 200) {
return Promise.reject(new Error(message));
}
console.log('正常拿到数据', res.data);
return res.data;
}, error => {
// 这里捕获请求取消的错误
if (axios.isCancel(error)) {
console.log('request cancel ', error)
return new Promise(() => { })
}
// 例如断网、跨域、状态码问题的报错
// error.response.status 这里可以拿到接口真正的状态码,还是要看后端是怎么设计接口的
return Promise.reject(new Error(error));
})
let request1Dom = request1
let request2Dom = request2
request1Dom.onclick = function () {
request({
url: 'list',
method: 'get',
requestCancelOpen: true
})
}
request2Dom.onclick = function () {
request({
url: 'list',
method: 'get'
})
}
</script>
</body>
</html>
例子使用方式,网络调整成慢速3G,然后疯狂点击按钮。
使用场景,实时搜索栏,光用去抖还是不够的,因为你无法保证前面的请求时间一定在下次请求之前到达。
fetch方案需要使用原生的AbortController,使用方式几乎一样