同学,请实现一个扫码登录

马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦!

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

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

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

0. 什么叫轮询?

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

1. 轮询的方案?

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

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

遇到的问题:

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

轮询: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的地方。于是百度了一番,找到一篇掘友的文章,为了表示尊敬我原封不动的搬到我的代码里了,哈哈!

axios取消请求 复制代码
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。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!

相关推荐
代码不加糖2 分钟前
js中不会冒泡的事件有哪些?
前端·javascript·vue.js
懂懂tty17 分钟前
Vue2与Vue3之间API差异
前端·javascript·vue.js
老毛肚1 小时前
软件测试期末考试
vue.js
小二·1 小时前
Next.js 15 全栈开发实战
开发语言·javascript·ecmascript
杨若瑜2 小时前
本地开发环境慢?localhost的锅!
vue.js
Rain5092 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
拾年2753 小时前
从零手写 Ajax:用原生 XHR 搭建前后端交互全流程
前端·javascript·ajax
拉勾科研工作室3 小时前
区块链工程毕业论文题目【249个】
开发语言·javascript
小林ixn3 小时前
你以为你懂 + 号?看完这篇 Bun + TS 实战,才发现以前全写错了
前端·javascript·typescript
jvxiao5 小时前
你真的懂作用域吗?从编译原理角度深度 JS 的作用域
前端·javascript