如何实现封装一个验证码组件?

唠一唠

验证码我们平时应该不少见到,以前一直好奇它是怎么实现的,经过一番摸索。原来其原理就是通过HTML5 Canvas API也就是我们说的画布,我们可以通过算法随机生成一系列字符、数字或图形,并将它们以扭曲、拉伸、旋转、添加噪点,以增加机器识别难度。对于更高级的验证码,可以使用SVG或者其他矢量图技术来创建复杂且难以解析的图形。不过,我目前还不会,哈哈哈。有大佬会的可以在评论区发出来哈,我也借鉴学习一下 ^.^ 。

目标效果

代码实现

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

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

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

1. 准备画布模版

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

2. 准备验证码字符

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

3. 准备生成随机数函数

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

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

  • 生成随机颜色
arduino 复制代码
// 随机颜色
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})`;
};

5. 绘制图片

ini 复制代码
// 定义一个绘制验证码的函数 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;
};

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

save() 方法:

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

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

restore() 方法:

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

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

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

最终整体代码

ini 复制代码
<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> 

在其他组件中使用

typescript 复制代码
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");
  }
};

最终效果:

如果觉得上述文章对你有帮助,请给博主点个赞哦,你的支持就是我最大的动力♥(ˆ◡ˆԅ)。有不对的地方欢迎大佬们在评论区指出哦。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端