如何使用setInterval和setTimeout去代替webSocket进行轮询调用接口查询

最近leader扔给我一个扫码登录的需求,我一看有点来劲了。一来做了多年前端,类似的需求还没有接触过,平时做的多的页面需求和改改bug对自身能力显然是无法提升的。二来扫码登录的功能很多应用都有做过,常见的微信扫码登录,也挺好奇具体如何实现。我大概看了一遍需求文档,写的挺详细的,流程图也标明了各端的交互流程。由于内网开发,产品流程图也忘记截图了,此处在网上找到的一个大概的流程图:

主要涉及到的是pc端、手机端和后台服务端。由于听产品同事说手机端由原生端(安卓和IOS)来实现,因此我这边只需要开发pc端就行,工作量直接减半有没有。做过该功能的小伙伴肯定了解,pc端的实现还是比较简单的,主要就是开启轮询查询后台扫码状态,然后做对应的提示或登录成功后跳转首页。

扫码登录的需求在前端主要难点在轮询上

0. 什么叫轮询?

所谓的轮询就是,由后端维护某个状态,或是一种连续多篇的数据(如分页、分段),由前端决定按序访问的方式将所有片段依次查询,直到后端给出终止状态的响应(结束状态、分页的最后一页等)。

1. 轮询的方案?

一般有两种解决方案:一种是使用websocket,可以让后端主动推送数据到前端;还有一种是前端主动轮询(上网查了下细分为长轮询和短轮询),通过大家熟悉的定时器(setIntervalsetTimeout)实现。

由于项目暂未用到websocket,且长轮询需要后台配合,所以直接采用短轮询(定时器)开撸了。

遇到的问题:

1、由于看需求文档上交互流程比较清晰,最开始没去网上查找实现方案,自己直接整了一版setInterval的轮询实现。在跟后台联调的过程中发现定时器每1s请求一次接口,发现很多接口没等响应就开启下一次的请求,很多请求都还在pending中,这样是不对的,对性能是很大消耗。于是想了下,可以通过setTimeout来优化,具体就是用setTimeout递归调用方式模拟setInterval的效果,达到只有上一次请求成功后才开启下一次的请求。

// 开启轮询
    async beginPolling() {
      if (this.isStop) return;
      try {
        const status = await this.getQrCodeStatus();
        if (!status) return;
        this.codeStatus = status;
        switch(this.codeStatus) {
          case '2':
            this.stopPolling();
            // 确认登录后,需前端修改状态
            this.codeStatus = '5';
            this.loading = true;
            // 走登录逻辑
            this.$emit('login', {
              qrcId: this.qrcId,
              encryptCSIIStr: this.macAddr
            })
            break;
          case '3':
            // 取消登录
            this.stopPolling();
            await this.getQrCode();
            break;
          case '4':
            // 二维码失效
            this.stopPolling();
            break;
          default:
            break;
        }
        this.timer = setTimeout(this.beginPolling);
      } catch(err) {
        console.log(err);
        this.stopPolling();
      }
    },

2、在自测了过程中又发现了另外一个问题,stopPolling方法中clearTimeout似乎无法阻止setTimeout的执行,二维码失效后请求仍在不停发出,这就很奇怪了。上网搜索了一番,发现一篇文章(很遗憾,已经找不到是哪篇文章了!)记录了这个问题:大概意思是虽然clearTimeout已经清除了定时器,但此时有请求已经在进行中,导致再次进入了循环体,重新开启了定时器。解决办法就是,需要手动声明一个标识位isStop来阻止循环体的执行。

    stopPolling() {
      if (this.timer) {
        clearTimeout(this.timer);
        this.timer = null;
        // 标记终止轮询(仅clearTimeout无法阻止)
        this.isStop = true;
      }
    },

试了下确实达到效果了,但其实这个问题产生的具体原因我还是有些模糊的,希望遇到过相关问题的大佬能指点一下,感激不尽!

3、解决了上面提到的问题,就在以为万事大吉,只待提测的时候。后台同事发现了一个问题(点赞后台同事的尽责之心):他在反复切换登录方式(扫码登录<->账号密码登录)的过程中,发现后台日志有一段时间打印的qrcId不是最新的。然后我这边试了下,确实在切换频率过高时,此时有未完成的请求仍在进行中,导致qrcId被重新赋值了。虽然已经在beforeDestroy里调用了stopPolling清除定时器,但此时请求是未停止的。聪明的小伙伴们肯定想到axioscancelToken可以取消未完成的请求,但我实际也并没有用过,而且项目里也没有可以表演Ctrl+CCtrl+V的地方。于是百度了一番,找到一篇掘友的文章,为了表示尊敬我原封不动的搬到我的代码里了,哈哈!

import axios from "axios";
const CancelToken = axios.CancelToken;

const cancelTokenMixin = {
  data() {
    return {
      cancelToken: null, // cancelToken实例
      cancel: null, // cancel方法
    };
  },
  created() {
    this.newCancelToken();
  },
  beforeDestroy() {
    //离开页面前清空所有请求
    this.cancel("取消请求");
  },
  methods: {
    //创建新CancelToken
    newCancelToken() {
      this.cancelToken = new CancelToken((c) => {
        this.cancel = c;
      });
    },
  },
};
export default cancelTokenMixin;

掘友文章[:](在vue项目中取消axios请求(单个和全局) - 掘金 (juejin.cn))

在组件里引入mixin,另外在请求时传入cancelToken实例,确实达到效果了。此时再次切换登录方式,之前的未完成的请求已被取消,也就无法再篡改qrcId。写到此处,我发现问题2也是未完成的请求导致的,那么是否可以不用isStop标识,直接在stopPolling中调用this.cancel("取消请求");不是更好吗?

完整代码如下:

import sunev from 'sunev'; // 全局公共方法库
import cancelTokenMixin from "@/utils/cancelTokenMixin"; // axios取消请求

export default {
  props: {
    loginType: {
      type: String,
      default: 'code'
    }
  },
  mixins: [cancelTokenMixin],
  data() {
    return {
      qrcId: '', // 二维码标识
      qrcBase64: '', // 二维码base64图片
      macAddr: '', // mac地址
      loading: false,
      isStop: false,
      codeStatus: '0',
      qrStatusList: [
        {
          status: '-1',
          icon: 'error',
          color: '#ed7b2f',
          svgClass: 'icon-error-small',
          text: '二维码生成失败\n请刷新重试',
          refresh: true
        },
        { status: '0', icon: '', text: '', refresh: false },
        {
          status: '1',
          icon: 'scan',
          color: '#2986ff',
          svgClass: 'icon-scan-small',
          text: '扫描成功\n请在移动端确认',
          refresh: false
        },
        {
          status: '2',
          icon: 'confirm',
          color: '#2986ff',
          svgClass: 'icon-confirm-small',
          text: '移动端确认登录',
          refresh: false
        },
        {
          status: '3',
          icon: 'cancel',
          text: '移动端已取消',
          refresh: false
        },
        {
          status: '4',
          icon: 'error',
          color: '#ed7b2f',
          svgClass: 'icon-error-small',
          text: '二维码已失效\n请刷新重试',
          refresh: true
        },
        {
          status: '5',
          icon: 'success',
          color: '#2986ff',
          svgClass: 'icon-success-small',
          text: '登录成功',
          refresh: false
        },
        {
          status: '6',
          icon: 'error',
          color: '#ed7b2f',
          svgClass: 'icon-error-small',
          text: '登录失败\n请刷新重试',
          refresh: true
        }
      ],
      errMsg: ''
    }
  },
  async created() {
    try {
      await this.getQrCode();
      this.beginPolling();
    } catch(err) {
      console.log(err);
    }
  },
  computed: {
    // 当前状态
    curQrStatus() {
      const statusObj = this.qrStatusList.find(item => item.status === this.codeStatus);
      if (this.errMsg) {
        statusObj.text = this.errMsg;
      }
      return statusObj;
    }
  },
  methods: {
    // 开启轮询
    async beginPolling() {
      if (this.isStop) return;
      try {
        const status = await this.getQrCodeStatus();
        if (!status) return;
        this.codeStatus = status;
        switch(this.codeStatus) {
          case '2':
            this.stopPolling();
            // 确认登录后,需前端修改状态
            this.codeStatus = '5';
            this.loading = true;
            // 走登录逻辑
            this.$emit('login', {
              qrcId: this.qrcId,
              encryptCSIIStr: this.macAddr
            })
            break;
          case '3':
            // 取消登录
            this.stopPolling();
            await this.getQrCode();
            break;
          case '4':
            // 二维码失效
            this.stopPolling();
            break;
          default:
            break;
        }
        this.timer = setTimeout(this.beginPolling);
      } catch(err) {
        console.log(err);
        this.stopPolling();
      }
    },
    // 暂停轮询
    stopPolling() {
      if (this.timer) {
        clearTimeout(this.timer);
        this.timer = null;
        // 标记终止轮询(仅clearTimeout无法阻止)
        this.isStop = true;
      }
    },
    // 获取二维码base64
    async getQrCode() {
      this.reset();
      this.loading = true;
      try {
        const params = {
          encryptCSIIStr: this.macAddr
        }
        const res = await sunev.$https.post(
          'sunev/LoginQRCGen',
          { isLoading: false, cancelToken: this.cancelToken },
          params
        )
        if (res.qrcId) {
          this.qrcId = res.qrcId;
          this.qrcBase64 = res.qrcBase64;
        } else {
          this.stopPolling();
        }
      } catch(err) {
        this.errMsg = err.message;
        this.stopPolling();
      }
    },
    // 获取二维码状态
    async getQrCodeStatus() {
      try {
        const params = {
          encryptCSIIStr: this.macAddr
        }
        const res = await sunev.$https.post(
          'sunev/LoginQRCQry',
          { isLoading: false, cancelToken: this.cancelToken },
          params
        )
        return res.status;
      } catch(err) {
        this.stopPolling();
      }
    },
    // 刷新二维码
    async refresh() {
      await this.getQrCode();
      this.beginPolling();
    },
    // 切换登录类型
    toggle() {
      this.$emit('toggleLoginType');
    },
    // 重置
    reset() {
      this.isStop = false;
      this.codeStatus = '0';
      this.errMsg = '';
    },
    beforeDestroy() {
      this.stopPolling();
    }
  }
}

ps:

1、由于是老项目了,登录界面逻辑较多,避免臃肿,二维码登录拆分成单独组件实现

2、由于项目组在内网开发,以下代码都是一行行重新手打的,不是很重要的html和css部分就省略了

后记:

由于此需求并不着急上线,暂未提测,所以还不知测试同事会提出怎样的bug。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
天行健王春城老师6 小时前
基于TRIZ的教育机器人功能创新
经验分享·机器人
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js