【场景方案】关于前端对接口行为的控制合集:轮番查询、并发请求、服务端通知、token无感刷新、请求取消

本文介绍了多种接口控制策略,包括前后端统一标识避免重复展示、控制并发请求、请求取消、服务端通知以及token无感刷新。对于并发请求,推荐使用Promise.all或自定义并发控制。此外,探讨了WebSocket和SSE推送的主动通知方案,并提供了token刷新的模拟代码,以及请求取消的实现。这些方法能有效提高接口交互的效率和用户体验。
摘要由CSDN通过智能技术生成

前言

以下收集了我工作以来遇到的对接口行为控制的操作方案,有一些暂时记不得了,等以后想到了再做补充。


轮番查询

一般轮番查询分情况看,如果是那种返回的数据大致只有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,使用方式几乎一样

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值