当前位置:   article > 正文

封装验证码组件的简易教程

封装验证码组件的简易教程

前言

验证码我们平时应该不少见到,其原理就是通过HTML5 Canvas API也就是我们说的画布,我们可以通过算法随机生成一系列字符、数字或图形,并将它们以扭曲、拉伸、旋转、添加噪点,以增加机器识别难度。对于更高级的验证码,可以使用SVG或者其他矢量图技术来创建复杂且难以解析的图形。

目标效果

在这里插入图片描述

代码实现

总的来说主要分为以下五步:

  • 准备画布模版
  • 准备验证码字符
  • 准备生成随机数函数
  • 准备生成随机颜色函数
  • 绘制图片

下面我们分别从上面五步来实现一个验证码组件

1. 准备画布模版

  • 画布也就是我们之前说的使用 HTML5中的 Canvas API,实现视频弹幕也可以通过它来实现。
<template>
  <div class="img-verify">
    <!-- 画布,绑定一个点击事件,用于后续验证码的刷新 -->
    <canvas ref="verify" :width="width" :height="height" @click="handleDraw"></canvas>
  </div>
</template>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2. 准备验证码字符

  • 准备好我们需要的验证码字符和存放验证码的变量
const state = reactive({
  str: "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", // 字符串
  imgCode: "", // 初始化验证码为空
});
  • 1
  • 2
  • 3
  • 4

3. 准备生成随机数函数

  • 用于后续生成各种随机的变量
// 随机数
const randomNum = (min: number, max: number): number => {
  return Math.floor(Math.random() * (max - min) + min);
};
  • 1
  • 2
  • 3
  • 4

4. 准备生成随机颜色函数

  • 生成随机颜色
// 随机颜色
const randomColor = (min: number, max: number) => {
  const r = randomNum(min, max);
  const g = randomNum(min, max);
  const b = randomNum(min, max);
  return `rgb(${r},${g},${b})`;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

5. 绘制图片

// 定义一个绘制验证码的函数 draw  
const draw = () => {
  // 获取 canvas 上下文,用于绘图
  const ctx = verify.value.getContext("2d");

  // 生成背景颜色,设置为浅色调(RGB范围在200-230之间)
  ctx.fillStyle = randomColor(200, 230);

  // 填充整个 canvas 背景区域
  ctx.fillRect(0, 0, state.width, state.height);

  // 初始化验证码字符串变量 imgCode
  let imgCode = "";

  // 遍历4次以绘制4个随机字符
  for (let i = 0; i < 4; i++) {
    // 随机选取字符集中的一个字符
    const text = state.str[randomNum(0, state.str.length)];

    // 将该字符添加到验证码字符串中
    imgCode += text;

    // 设置随机字体大小(18px - 40px)
    const fontSize = randomNum(18, 40);

    // 设置随机旋转角度(-30度至30度)
    const deg = randomNum(-30, 30);

    // 设置字体样式、大小和颜色
    ctx.font = `${fontSize}px Simhei`;
    ctx.textBaseline = "top";
    ctx.fillStyle = randomColor(80, 150);

    // 保存当前画布状态,以便进行平移、旋转等操作而不影响后续绘制
    ctx.save();

    // 对每个字符进行坐标平移,并根据旋转角度进行旋转
    ctx.translate(30 * i + 15, 15);
    ctx.rotate((deg * Math.PI) / 180);

    // 在当前位置绘制填充的文本(偏移量防止超出边框)
    ctx.fillText(text, -15 + 5, -15);

    // 恢复之前保存的画布状态,撤销平移和旋转
    ctx.restore();
  }

  // 绘制5条随机干扰线,颜色设置为较浅色系
  for (let i = 0; i < 5; i++) {
    ctx.beginPath();
    ctx.moveTo(randomNum(0, state.width), randomNum(0, state.height));
    ctx.lineTo(randomNum(0, state.width), randomNum(0, state.height));
    ctx.strokeStyle = randomColor(180, 230);
    ctx.closePath(); // 这一行实际上在绘制直线时是可选的
    ctx.stroke(); // 实际上绘制出干扰线
  }

  // 绘制40个随机位置的小圆点作为干扰元素,颜色同样设置为较浅色系
  for (let i = 0; i < 40; i++) {
    ctx.beginPath();
    // 创建一个圆形路径,指定圆心坐标与半径
    ctx.arc(randomNum(0, state.width), randomNum(0, state.height), 1, 0, 2 * Math.PI);
    ctx.closePath(); // 同样,在绘制封闭图形如圆时是可选的

    // 设置小圆点的颜色
    ctx.fillStyle = randomColor(150, 200);

    // 填充该圆形路径,形成实心小圆点
    ctx.fill();
  }

  // 返回生成的验证码字符串
  return imgCode;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

这里我重点解释一下上述ctx.save()ctx.restore()的用法。它们在HTML5 Canvas API中,是两个非常重要的方法,用于保存和恢复Canvas画布的状态。

save() 方法:

  • 当调用 ctx.save() 时,Canvas会将当前的绘图环境状态(包括但不限于以下内容)保存到一个内部的栈结构中:

    • 当前的变换矩阵(例如平移、旋转、缩放等)
    • 当前的填充样式、描边样式、阴影样式
    • 全局Alpha值(透明度)
    • 其他图形属性如裁剪区域等

restore() 方法:

  • 调用 ctx.restore() 时,Canvas会从这个内部栈中弹出最近一次保存的状态,并将其设置为当前的绘图环境状态。
  • 这意味着之前通过 save() 存储的所有属性都会被还原到保存时的状态,取消掉在这两者之间所做的任何改变。

简单来说,当你在绘制过程中需要对画布执行一些临时性的操作(如进行复杂的变换或临时更改颜色),但不希望这些操作影响后续的绘制时,可以先调用 save() 保存当前状态,然后执行所需的操作。完成这部分绘制后,调用 restore() 来确保画布状态回到之前的点,这样下一次绘制就会不受刚才那些临时操作的影响。

举个例子,在上述验证码生成代码片段中,每次循环绘制字符之前都调用了 ctx.save(),目的是为了旋转和定位每个字符而不影响其他字符的位置和角度。字符绘制完成后使用 ctx.restore() 恢复画布原始状态,以便下一个字符能够按照新的随机位置和角度独立绘制。

最终整体代码

<template>
  <div class="img-verify">
    <!-- 画布,绑定一个点击事件,用于刷新验证码 -->
    <canvas ref="verify" :width="width" :height="height" @click="handleDraw"></canvas>
  </div>
</template>
<script>
import { reactive, onMounted, ref, toRefs } from "vue";
export default {
  setup() {
    const verify = ref(null);
    const state = reactive({
      str: "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", // 字符串
      width: 110,
      height: 40,
      imgCode: "", // 初始化验证码为空
    });
    onMounted(() => {
      if (verify.value) {
        // 初始化绘制图片验证码
        state.imgCode = draw();
      }
    });

    // 点击图片重新绘制
    const handleDraw = () => {
      state.imgCode = draw();
    };

    // 随机数
    const randomNum = (min: number, max: number): number => {
      return Math.floor(Math.random() * (max - min) + min);
    };

    // 随机颜色
    const randomColor = (min: number, max: number) => {
      const r = randomNum(min, max);
      const g = randomNum(min, max);
      const b = randomNum(min, max);
      return `rgb(${r},${g},${b})`;
    };

    // 定义一个绘制验证码的函数 draw  
    const draw = () => {
      // 获取 canvas 上下文,用于绘图
      const ctx = verify.value.getContext("2d");

      // 生成背景颜色,设置为浅色调(RGB范围在200-230之间)
      ctx.fillStyle = randomColor(200, 230);

      // 填充整个 canvas 背景区域
      ctx.fillRect(0, 0, state.width, state.height);

      // 初始化验证码字符串变量 imgCode
      let imgCode = "";

      // 遍历4次以绘制4个随机字符
      for (let i = 0; i < 4; i++) {
        // 随机选取字符集中的一个字符
        const text = state.str[randomNum(0, state.str.length)];

        // 将该字符添加到验证码字符串中
        imgCode += text;

        // 设置随机字体大小(18px - 40px)
        const fontSize = randomNum(18, 40);

        // 设置随机旋转角度(-30度至30度)
        const deg = randomNum(-30, 30);

        // 设置字体样式、大小和颜色
        ctx.font = `${fontSize}px Simhei`;
        ctx.textBaseline = "top";
        ctx.fillStyle = randomColor(80, 150);

        // 保存当前画布状态,以便进行平移、旋转等操作而不影响后续绘制
        ctx.save();

        // 对每个字符进行坐标平移,并根据旋转角度进行旋转
        ctx.translate(30 * i + 15, 15);
        ctx.rotate((deg * Math.PI) / 180);

        // 在当前位置绘制填充的文本(偏移量防止超出边框)
        ctx.fillText(text, -15 + 5, -15);

        // 恢复之前保存的画布状态,撤销平移和旋转
        ctx.restore();
      }

      // 绘制5条随机干扰线,颜色设置为较浅色系
      for (let i = 0; i < 5; i++) {
        ctx.beginPath();
        ctx.moveTo(randomNum(0, state.width), randomNum(0, state.height));
        ctx.lineTo(randomNum(0, state.width), randomNum(0, state.height));
        ctx.strokeStyle = randomColor(180, 230);
        ctx.closePath(); // 这一行实际上在绘制直线时是可选的
        ctx.stroke(); // 实际上绘制出干扰线
      }

      // 绘制40个随机位置的小圆点作为干扰元素,颜色同样设置为较浅色系
      for (let i = 0; i < 40; i++) {
        ctx.beginPath();
        // 创建一个圆形路径,指定圆心坐标与半径
        ctx.arc(randomNum(0, state.width), randomNum(0, state.height), 1, 0, 2 * Math.PI);
        ctx.closePath(); // 同样,在绘制封闭图形如圆时是可选的

        // 设置小圆点的颜色
        ctx.fillStyle = randomColor(150, 200);

        // 填充该圆形路径,形成实心小圆点
        ctx.fill();
      }

      // 返回生成的验证码字符串
      return imgCode;
    };

    return {
      ...toRefs(state),  // toRefs为了防止结构数据丢失响应性
      verify,
      handleDraw,
    };
  },
};

</script>
<style>
/* 设置鼠标悬停样式 */
.img-verify canvas {
  cursor: pointer;
}
</style> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132

在其他组件中使用

HHTML:
<!-- 模版,点击刷新验证码 -->
<div class="imgCode">
  <VueImgVerify ref="verifyRef" />
</div>

JS:
// 引入组件
import VueImgVerify from '@/components/VueImgVerify.vue';


// 便于拿到 verifyRef 组件内的实例属性
const verifyRef = ref<any>(null);

// 这里是我项目中点击注册时会触发的验证,具体以自己的代码来写
const onSubmit = async () => {
  // 使用 nextTick 确保组件渲染后再访问 verifyRef
  await nextTick();

  // 确保 verifyRef 不为 null
  if (verifyRef.value) {
    // 生成的图片验证码的文字等于验证码组件生成的验证码
    state.imgCode = verifyRef.value.imgCode || "";
    // 如果验证码组件生成的验证码的小写 != 用户输入的验证码的小写,则提示错误
    if (
      verifyRef.value.imgCode.toLowerCase() != state.verify.toLowerCase()
    ) {
      console.log("verifyRef.value.imgCode", verifyRef.value.imgCode);
      showFailToast("验证码错误");
      console.log("Generated Captcha:", verifyRef.value.imgCode.toLowerCase());
      console.log("User Input Captcha:", state.verify.toLowerCase());

      return;
    }
    // 验证码匹配成功,注册=>注册成功

    // 发请求,将state.nickname,state.username,state.password数据传给后端
    await axios.post('/register', {
      nickname: state.nickname,
      username: state.username,
      password: state.password,
      isVip: state.isVip,
      userType: state.userType
    });

    showSuccessToast('注册成功!');

    // 1s后跳转到登录页面
    setTimeout(() => {
      router.push('/login');
    }, 1000);
  } else {
    // 处理 verifyRef 为 null 的情况
    console.error("verifyRef is null");
  }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

最终效果:

在这里插入图片描述

附件资料:点此下载

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/288612
推荐阅读
相关标签
  

闽ICP备14008679号