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

news2024/11/25 22:46:50

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

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

主要涉及到的是 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。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!

最后

欢迎关注"所谓前端"微信公众号,大家一起交流
点击扫码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1451411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

optee imx8mm

总仓库 git clone https://github.com/Xsyin/imx8mqevk.git -b container_region 替换imx8mqevk中的optee-client git clone https://github.com/nxp-imx/imx-optee-client.git -b lf-5.15.32_2.0.0 用 5.15.32 kernel 会有如下报错&#xff0c;需要将optee os升级到分支 lf-…

Istio复习总结:xDS协议、Istio Pilot源码、Istio落地问题总结

1、xDS协议 1&#xff09;、xDS是什么 xDS是一类发现服务的总称&#xff0c;包含LDS、RDS、CDS、EDS以及SDS。Envoy通过xDS API可以动态获取Listener&#xff08;监听器&#xff09;、Route&#xff08;路由&#xff09;、Cluster&#xff08;集群&#xff09;、Endpoint&…

不同AI分析错误代码的差异:谁更胜一筹?谁才是最强者?结果出乎意料!

先祝大家新春快乐&#xff0c;我已经提前三天上班了~~为了年后新框架能上线运行&#xff0c;这几天没人打扰&#xff0c;能安静地冲一下代码&#xff0c;嘎嘎嘎。 准备 错误代码&#xff1a; ... foreach($arr_config[path] as $value_path) {if(file_exists($value_path)){r…

制作怎么自己搭建一个网站

制作怎么自己搭建一个网站 一.领取一个免费域名和SSL证书&#xff0c;和CDN 1.打开网站链接&#xff1a;https://www.rainyun.com/ycpcp_ 首先创建一个CDN&#xff0c;这里以我加速域名“cdntest.biliwind.com 1”为例 这里就要填写 cdntest.biliwind.com 1 &#xff0c;而…

Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(上)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

FuckIt.py库让你的代码从此远离bug

今天给你推荐的这个库叫 “FuckIt.py”&#xff0c;名字一看就是很黄很暴力的那种&#xff0c;作者是这样介绍它的&#xff1a; FuckIt.py uses state-of-the-art technology to make sure your Python code runs whether it has any right to or not. Some code has an error…

无心剑中译莎士比亚《劝君缔结连理枝》

莎士比亚十四行诗第8首 Sonnet 8 - 劝君缔结连理枝 Music to hear, why hear’st thou music sadly? Sweets with sweets war not, joy delights in joy. Why lovest thou that which thou receivest not gladly, Or else receivest with pleasure thine annoy? If the tru…

BUGKU-WEB bp

题目描述 题目截图如下&#xff1a; 进入场景看看&#xff1a; 解题思路 提示说&#xff1a;弱密码top1000&#xff1f;z???(爆破?)先看看源码有没有提示 相关工具 Burp Suit 爆破top1000字典&#xff0c;点击下载 解题步骤 随便测试账号密码admin、admin 得到提…

scIMC:scRNA-seq插补方法基准

在scRNA-seq中一个主要的挑战即为“dropout”事件&#xff0c;它扭曲了基因表达&#xff0c;显著影响了单细胞转录组的下游分析。为了解决这个问题&#xff0c;已经做了很多努力&#xff0c;并开发了几种基于模型和基于深度学习的scRNA-seq插补方法。但是&#xff0c;目前还缺乏…

彻底理解无刷电机

前言 现在很多设备都是搭载的无刷电机而不是有刷电机了&#xff0c;为啥&#xff1f;性能好啊&#xff01; 引入 同性相斥异性相吸 可以看出&#xff0c;只要改变磁铁的极性&#xff0c;电机就能转起来 那 怎么改变磁铁极性呢&#xff1f; 右手螺旋定则可以根据电流的流向…

ch3-homework-基于InternLM和LangChain搭建自己的知识库

ch3-homework-基于InternLM和LangChain搭建自己的知识库 复现课程知识库助手搭建过程先看结果环境配置语料开源词向量模型Sentence Transformer知识库搭建InternLM 接入 LangChain构建检索问答链&#xff0c;并基于Gradio框架部署 基础作业&#xff1a; 复现课程知识库助手搭建…

【Day42】代码随想录之动态规划0-1背包_416. 分割等和子集

文章目录 动态规划理论基础动规五部曲&#xff1a;出现结果不正确&#xff1a; 416. 分割等和子集 动态规划理论基础 动规五部曲&#xff1a; 确定dp数组 下标及dp[i] 的含义。递推公式&#xff1a;比如斐波那契数列 dp[i] dp[i-1] dp[i-2]。初始化dp数组。确定遍历顺序&am…

Android---Jetpack Compose学习006

1. 点击 clickable 修饰符允许应用检测对已应用该修饰符的元素的点击。 示例&#xff1a;点击控件&#xff0c;使得内容发生改变 class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setCo…

C++数据结构与算法——双指针法

C第二阶段——数据结构和算法&#xff0c;之前学过一点点数据结构&#xff0c;当时是基于Python来学习的&#xff0c;现在基于C查漏补缺&#xff0c;尤其是树的部分。这一部分计划一个月&#xff0c;主要利用代码随想录来学习&#xff0c;刷题使用力扣网站&#xff0c;不定时更…

0901多元函数的基本概念-多元函数微分法及其应用

文章目录 1 平面点集1.1 坐标平面1.2 平面点集1.3 邻域1.4 电与点集的关系1.5 聚点1.6 点集所属点的特征定义的平面点集 2 多元函数的概念2.1 定义2.2 值域2.3推广2.4 自然定义域2.5 二元函数的图形 3 多元函数的极限4 多元函数的连续性4.1 连续函数定义4.2 间断点定义4.3 多元…

【教程】C++语言基础学习笔记(八)——函数

写在前面&#xff1a; 如果文章对你有帮助&#xff0c;记得点赞关注加收藏一波&#xff0c;利于以后需要的时候复习&#xff0c;多谢支持&#xff01; 【C语言基础学习】系列文章 第一章 《项目与程序结构》 第二章 《数据类型》 第三章 《运算符》 第四章 《流程控制》 第五章…

2024】前端,该卷什么呢?_2024-02-16

2024已来&#xff0c;过去的 2023 可以说是具有里程碑意义的一年&#xff0c;ChatGPT 的炸裂式发展&#xff0c;很多大佬都亲自入场整活儿&#xff0c;你不得不说&#xff0c;人工智能时代的未来已来&#xff0c;大势所趋&#xff0c;不可阻挡。随着生成式AI的迅猛发展&#xf…

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

文章目录 一、起因二、代码1. 定义exchange和queue2. RabbitTemplate3. EnhancedCorrelationData4. 发送消息 环境如下 VersionSpringBoot3.2.1spring-amqp3.1.1RabbitMq3-management 一、起因 老版本的spring-amqp在CorrelationData上设置ConfirmCallback。但是今天却突然发…

RCS系统之:浅谈系统设计与开发

这是我在开发RCS系统中的一些个人感悟与心得&#xff0c;写出来与大家一起分享下。是想到什么写到什么&#xff0c;如果有什么不对的&#xff0c;欢迎大家一起探讨。 有些人喜欢把WMS系统下面的系统统称为RCS系统。 但我不是这么想的&#xff0c;我这里把WMS/ERP系统与AGV之间…

[office] Excel 数据库函数条件区域怎样设置 #笔记#笔记

Excel 数据库函数条件区域怎样设置 以下面的数据表格为例&#xff0c;对于条件区域的设置&#xff0c;有几方面需要注意的内容&#xff0c;下面就一起看看如何对Excel 数据库函数条件区域设置的吧。希望会大家有所帮助 以下面的数据表格为例&#xff0c;对于条件区域的设置&am…