手把手带你开发一款滑动验证码组件

3,408 阅读6分钟

前言

最近在工作之余,从零开发了一款滑动验证码组件,主要采用了vue + canvas进行开发.滑动验证码相信大家肯定会经常遇到,但是身为开发者的你,是否静下心来研究过它是如何实现的?

没有思考过它是如何实现的也不要慌,本篇文章将会手把手带你实现它,通过这个项目大家可以学习到以下几点:

  • 前端封装组件的基本思路
  • canvas的基本知识和使用方法
  • vue的基本知识和使用方法
  • 滑块验证码的基本实现思路
  • 如何完成div的拖拽

相信这篇文章一定会给大家带来启发~~

效果演示

aaaa(1).gif

滑动验证组件基本使用

上图是实现的滑动验证码的演示效果,它还支持场景化的配置,接下来将介绍组件的基本使用方式,如果有技术基础的同学可以直接跳到技术实现部分。

为了让滑动验证码组件在多场景使用,向外暴露了很多可配置的属性,来满足不同的应用场景。

NameDescriptionTypeDefault
visiblity是否可见Booleanfalse
width宽度Number300
height高度Number200
l滑块的长度Number42
r滑块的半径Number9
tolerant滑块的容错值Number5
onSuccess成功回调Function() => {}
onRefresh刷新回调Function() => {}
onFail失败回调Function() => {}
closed关闭回调Function() => {}

技术实现

接下来将会以我的思路来带大家实现一款滑动验证码组件,如果有更好的思路和想法或者对我的实现思路有疑问欢迎评论区交流讨论~~

1. 明确功能

在开发前我们首先要明确要实现哪些功能:

  1. 随机图片(这里不做过多描述,采用Picsum实现
  2. 位置随机的镂空图案绘制
  3. 利用canvas进行图案裁剪
  4. 可滑动控制的拖拽滑块
  5. 图片加载时的loading效果
  6. 刷新按钮的点击
  7. 验证拖拽位置是否满足要求

image.png

2. 基本框架

我们先搭建出基本的框架

<div v-if="visiblity" class="slidingWrap">
    <div class="slidingTitle"></div>
    <!-- 画布区域 -->
    <div class="canvasArea">
      <!-- 主要渲染整体背景及镂空图案 -->
      <canvas class="canvas-bg" :width="width" :height="height"></canvas>
      <canvas
        class="block"
        :height="height"
        :style="{ left: `${blockLeft}px` }"
      ></canvas>
    </div>
    <!-- 底部滑块 -->
    <div
      :class="['sliderWrap', status]"
      :style="{ width: `${width}px` }"
      @mouseleave="handleDrapUp"
    >
      <div
        class="progress"
        :style="{
          width: `${blockLeft}px`
        }"
      ></div>
      <div
        class="slider-block"
        ref="slider"
        :style="{ left: `${blockLeft}px` }"
        @mousedown="handleDrapDown"
        @mouseup="handleDrapUp"
        @mousemove="handleDragMove"
      >
        &rarr;
      </div>
    </div>
    <!-- 刷新按钮 -->
    <div class="refresh" @click="onRefreshBtnClick"></div>
    <!-- 加载中提示 -->
    <div
      class="loading"
      v-show="isLoading"
      :style="{ width: `${width}px`, height: `${height}px` }"
    >
      <img
        src="https://img1.baidu.com/it/u=909624486,4182680343&fm=26&fmt=auto"
        alt=""
      />
      加载中...
    </div>
  </div>

然后定义一些支持外界配置的属性

  props: {
    // 组件是否可见
    visiblity: {
      type: Boolean,
      default: false
    },
    // 组件的宽度
    width: {
      type: Number,
      default: 300
    },
    // 组件的高度
    height: {
      type: Number,
      default: 200
    },
    // 滑块的长度
    l: {
      type: Number,
      default: 42
    },
    // 滑块的半径
    r: {
      type: Number,
      default: 9
    },
    // 滑块的容错值
    tolerant: {
      type: Number,
      default: 5
    },
    // 成功时的回调
    onSuccess: {
      type: Function,
      default: () => {}
    },
    // 刷新时的回调
    onRefresh: {
      type: Function,
      default: () => {}
    },
    // 失败时的回调
    onFail: {
      type: Function,
      default: () => {}
    },
    // 关闭回调
    closed: {
      type: Function,
      default: () => {}
    }

3.实现镂空效果的图片

image.png

上图我们可以发现是一个不规则的图形,下面简单画一个草图

未命名文件 (2).png

在开始之前,我们首先要了解用到的api:

  • beginPath(): 当你想创建一个新的路径时,调用此方法
  • moveTo(): 将一个新的子路径的起始点移动到(x,y)坐标的方法
  • arc(): 绘制圆弧路径的方法
  • lineTo(): 使用直线连接子路径的终点到x,y坐标的方法
  • stroke(): 描边
  • fill(): 填充
  • clip(): 裁剪
  • save():通过将当前状态放入栈中,保存 canvas 全部状态的方法。
  • restore():通过在绘图状态栈中弹出顶端的状态,将 canvas 恢复到最近的保存状态的方法。
  • getImageData():返回一个ImageData对象,用来描述canvas区域隐含的像素数据
  • putImageData():将数据从已有的 ImageData 对象绘制到位图的方法
  • drawImage():在canvas上绘制图像。

代码实现如下:

drawPath(x, y, l, r, ctx, operation) {
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.arc(x + l / 2, y - r + 2, r, 0.72 * Math.PI, 2.26 * Math.PI);
      ctx.lineTo(x + l, y);
      ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * Math.PI, 2.78 * Math.PI);
      ctx.lineTo(x + l, y + l);
      ctx.lineTo(x, y + l);
      // anticlockwise为一个布尔值。为true时,是逆时针方向,否则顺时针方向
      ctx.arc(
        x + r - 2,
        y + l / 2,
        r + 0.4,
        2.76 * Math.PI,
        1.24 * Math.PI,
        true
      );
      ctx.lineTo(x, y);
      ctx.lineWidth = 2;
      ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
      ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
      ctx.stroke();
      ctx.globalCompositeOperation = "destination-over";
      // 判断是填充还是裁切, 裁切主要用于生成图案滑块
      operation === "fill" ? ctx.fill() : ctx.clip();
    }

通过该方法我们可以在canvas中画出对应的图案,细心的同学可以发现,我们在body中写了两个canvas标签,那么他们的作用分别是什么呢?

  • canvas-bg主要功能是绘制背景图片和镂空图案
  • block主要功能是对图案进行裁剪并进行移动

4. 初始化函数

/**
     * 1. 随机获取到图片
     * 2. 随机生成图片滑块位置
     * 3. 截取图片并移动到初始位置
     * 4. 画出镂空图案
     * 5. 拿到下方滑块的初始位置
     */
    initCanvas() {
      // 拿到canvas对象
      let canvas = document.getElementsByClassName("canvas-bg")[0];
      let blockcanvas = document.getElementsByClassName("block")[0];
      // 画笔
      let ctx = canvas.getContext("2d");
      let blockCtx = blockcanvas.getContext("2d");
      // 每次重新绘制图片时,清除画布原有内容
      ctx.clearRect(0, 0, this.width, this.height);
      blockcanvas.width = this.width;
      // 生成image对象
      let img = new Image();
      // 解决跨域问题
      img.crossOrigin = "";
      img.onload = () => {
        ctx.drawImage(img, 0, 0);
        blockCtx.drawImage(img, 0, 0);
        this.isLoading = false;
        const ImageData = blockCtx.getImageData(x, y1, this.l + 2 * this.r, y);
        // 裁剪后重置画布的状态
        blockCtx.restore();
        blockcanvas.width = this.l + 2 * this.r;
        blockCtx.putImageData(ImageData, 0, y1);
      };
      // 图片访问地址,后面增加时间戳,防止拿缓存
      img.src = `https://picsum.photos/${this.width}/${
        this.height
      }?time=${+new Date()}`;
      // x y 位置随机
      let { x, y } = this.getXY();
      this.drawPath(x, y, this.l, this.r, ctx, "fill");
      // 裁剪前保存画布的状态
      blockCtx.save();
      this.drawPath(x, y, this.l, this.r, blockCtx, "clip");
      const y1 = y - this.r * 2 - 1;
    }

我们知道,滑动验证码的主要功能就是判断是否人为操作,我们通常采用的方式就是随机验证,这里我们前端采用Math.random()方法进行实现,更安全的做法是通过后端进行返回,生成随机位置的函数如下:

    // 随机生成滑块的目标位置
    getXY() {
      let x =
        this.width / 3 +
        Math.random() * ((this.width * 3) / 4 - this.width / 3);
      let y =
        this.height / 3 +
        Math.random() * ((this.height * 3) / 5 - this.height / 3);
      this.x = x;
      return { x, y };
    }

这里限制了位置坐标的范围,让滑块的目标位置更加的合理,让目标位置在白色区域范围内:

未命名文件 (3).png

5.滑块的拖拽

完成以上代码后,页面布局基本为这个样子: 未命名文件 (4).png

观察我们可以发现,我们拖拽下方箭头时,block跟随滑块移动即可,那么如何实现一个滑块的拖拽呢?

我们可以想一下拖拽动作是如何发生的,按下鼠标左键后移动鼠标即可拖拽,鼠标释放完成拖拽,我们可以把它分解为三个动作:mousedownmousemovemouseup,我们监听三个动作即可完成相应的操作。

   handleDragMove(e) {
      if (!this.isDrop) return false;
      e.preventDefault();
      // 获取鼠标位置
      const eventX = e.clientX || e.touches[0].clientX;
      let moveX = eventX - this.blockInitLeft - 20;
      // 对滑块的位置进行风控处理
      if (moveX < 0 || moveX + 40 + 2 * this.r > this.width) return false;
      // 设置滑块的的位置及进度条的宽度
      this.blockLeft = moveX;
    },
    // 初始化未完成时,不可滑动
    // 滑动开始时,将状态置为等待状态
    handleDrapDown() {
      if (this.isLoading) return;
      this.isDrop = true;
      this.status = "wait";
    },
    // 拖拽动作完成之后执行
    handleDrapUp() {
      if (!this.isDrop) return;
      this.isDrop = false;
      // 判断是否拖拽的指定位置
      let val = Math.abs(this.x - this.blockLeft);
      this.status = val > this.tolerant ? "fail" : "success";
      // 执行相应的生命周期函数
      if (this.status === "fail") {
        this.onFail();
      } else {
        this.onSuccess();
      }
      // 1s后触发关闭回调或者刷新动作
      setTimeout(() => {
        if (this.status === "success" && this.visiblity) {
          this.closed();
          return;
        }
        this.handleRefresh();
      }, 1000);
    }

最后

滑块验证码组件的大致功能已经完成,还有一些细节方面需要优化(图片加载时的loading效果、刷新按钮的点击),在github上有完整的代码,感兴趣的可以参考一下~

有任何疑问或者更好的实现思路欢迎在评论区留言讨论~~