前端图形学实战: 从零开发一款轻量级滑动验证码组件(vue3 + vite版)

17,779 阅读9分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究

前言

hello, 大家好, 我是徐小夕, 今天又到了我们的博学时间。

本文是 100+前端几何学应用案例 专栏的第五篇文章, 之前和大家分享了如何从零实现几何画板以及几何画板的撤销 / 重做 / 图层管理等功能:

今天和大家分享一个新的图形学实战——滑动验证码的实现

你将收获

  • vue3组件的设计思路和技巧
  • canvas的常用绘图api用法
  • 滑动验证码组件的基本设计原理和实现过程
  • 如何从零发布vue3组件库

效果演示

按照笔者的写作习惯, 这里先和大家演示一下实现的效果:

2022-11-19 14.37.23.gif

正文

这篇文章我会带大家使用 vue3 + vite 来实现滑动验证码, 当然笔者之前也实现了基于 react 的滑动验证码组件, 感兴趣的可以参考我之前的文章:

同时也可以在 github 上查看到对应的实现代码:

github.com/MrXujiang/v…

技术实现

在着手开发组件之前我们需要提前明确组件的设计需求, 并制定合理的开放 api, 这里分享一下我总结的组件优雅设计的原则:

  • 可读性(代码格式统一清晰,注释完整,代码结构层次分明,编程范式使用得当)

  • 可用性(代码功能完整,在不同场景都能很好兼容,业务逻辑覆盖率)

  • 复用性(代码可以很好的被其他业务模块复用)

  • 可维护性(代码易于维护和扩展,并有一定的向下/向上兼容性)

  • 高性能(组件具有一定的性能, 如复杂场景的渲染, 计算等)

而我们滑动验证码组件要想让更多的人使用, 就必须做到一定程度的灵活可配置, 接下来是我总结的滑动验证码的核心 api:

  • 控制滑动验证码的显示隐藏(visible)
  • 控制滑动验证码的大小(width 和 height)
  • 控制滑块的样式(边长 l 和半径 r)
  • 滑块的提示文本(text)
  • 滑动验证码的图片数据源(imgUrls)
  • 验证成功的事件回调(onSuccess)
  • 验证失败的事件回调(onFail)
  • 用户自定义的验证逻辑(onCustomVertify)
  • 用户拖拽滑块时的回调(onDraw)
  • 刷新图片时的回调(onRefresh)

对应图示如下:

image.png

属性功能设计好之后我们来构想一下在vue项目中具体的使用方式:

<template>
  <div>
    <vertify 
      width="200" 
      height="80" 
      l="50" 
      r="5"
    ></vertify>
  </div>
</template>

<script setup>
// 业务逻辑...
</script>

以上基础准备好之后我们来可看一下滑动验证码的具体交互流程:

image.png

1.设计滑动验证码的基本骨架

这里我以vue3组合函数的方法来定义一下 vue-slider-veritfy 组件骨架:

<script lang="ts" setup>
import { ref, watch, onMounted } from "vue";

interface VertifyType {
  spliced: boolean;
  verified: boolean; // 简单验证拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
  left: number; // 滑块的移动位置
  destX: number; // 滑块的目标位置
}

interface IProps {
  width?: number;
  visible?: boolean;
  height?: number;
  refreshIcon?: string;
  l?: number;
  r?: number;
  imgUrl?: string;
  text?: string;
  /**
   * @description   拖拽滑块时的回调, 参数为当前滑块拖拽的距离
   * @default       (l: number):void => {}
   */
  onDraw?: (l: number) => {};
  /**
   * @description   用户的自定义验证逻辑
   * @default       (arg: VertifyType) => VertifyType
   */
  onCustomVertify?: (arg: VertifyType) => VertifyType;
  /**
   * @description   重制刷新前的回调
   * @default       ():void => {}
   */
  onBeforeRefresh?: () => void;
  /**
   * @description   验证成功回调
   * @default       ():void => {}
   */
  onSuccess?: VoidFunction;
  /**
   * @description   验证失败回调
   * @default       ():void => {}
   */
  onFail?: VoidFunction;
  /**
   * @description   刷新时回调
   * @default       ():void => {}
   */
  onRefresh?: VoidFunction;
}

// 定义默认值
const props = withDefaults(defineProps<IProps>(), {
  width: 320,
  visible: true,
  height: 160,
  refreshIcon: "http://cdn.dooring.cn/dr/icon12.png",
  l: 42,
  r: 9,
  imgUrl: "",
  text: "",
});

// 构建dom结构
<template>
  <div
    className="vertifyWrap"
  >
    <div className="canvasArea">
      <canvas ref="canvasRef"></canvas>
    </div>
    <div
      :className="sliderClass"
    >
      <div className="sliderMask">
        <div
          className="slider"
        >
          <div className="sliderIcon">&rarr;</div>
        </div>
      </div>
      <div className="sliderText">{{ textTip }}</div>
    </div>
    <div
      className="refreshIcon"
    ></div>
    <div
      className="loadingContainer"
    >
      <div className="loadingIcon"></div>
      <span>加载中...</span>
    </div>
  </div>
</template>

上面代码需要注意的就是 withDefaultsdefineProps , defineProps 主要用来定义 vue 组件的属性类型, withDefaults 可以用来定义组件属性的默认值, 这也是在设计 vue3 组件中常用的 api

2. 滑动验证码核心功能实现

接下来我们需要实现以下几个核心功能:

  • 镂空效果的 canvas 图片实现
  • 镂空图案 canvas 实现
  • 滑块移动和验证逻辑实现

上面的描述可能比较抽象,我画张图示意一下:

image.png

在开始编码之前我们需要对 canvas 有个基本的了解,建议不熟悉的朋友可以参考高效 canvas 学习文档: Canvas of MDN

由上图可知首先要解决的问题就是如何用 canvas 画不规则的图形,这里我简单的画个草图:

image.png

我们只需要使用 canvas 提供的 路径api 画出上图的路径,并将路径填充为任意半透明的颜色即可。建议大家不熟悉的可以先了解如下 api :

  • beginPath() 开始路径绘制
  • moveTo() 移动笔触到指定点
  • arc() 绘制弧形
  • lineTo() 画线
  • stroke() 描边
  • fill() 填充
  • clip() 裁切路径

由于实现方式比较固定, 偏 canvas 语法层, 这里直接分享一下代码:

const drawPath = (ctx: any, x: number, y: number, operation: "fill" | "clip") => {
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
  ctx.lineTo(x + l, y);
  ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
  ctx.lineTo(x + l, y + l);
  ctx.lineTo(x, y + l);
  ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
  ctx.lineTo(x, y);
  ctx.lineWidth = 2;
  ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
  ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
  ctx.stroke();
  ctx.globalCompositeOperation = "destination-over";
  operation === "fill" ? ctx.fill() : ctx.clip();
};

这里需要补充的一点是 canvasglobalCompositeOperation 属性,它的主要目的是设置如何将一个源(新的)图像绘制到目标(已有)的图像上。

  • 源图像 = 我们打算放置到画布上的绘图

  • 目标图像 = 我们已经放置在画布上的绘图

w3c上有个形象的例子:

image.png

这里之所以设置该属性是为了让镂空的形状不受背景底图的影响并覆盖在背景底图的上方。如下:

image.png

接下来我们只需要将图片绘制到画布上即可:

const canvasCtx = canvasRef.current.getContext('2d')
// 绘制镂空形状
drawPath(canvasCtx, 50, 50, 'fill')

// 画入图片
canvasCtx.drawImage(img, 0, 0, width, height)

接下来我们只需要用 javascript 实现随机图片和随机位置即可。

我们再来实现一下镂空效果:

const blockCtx = blockRef.value.getContext('2d')
drawPath(blockCtx, 50, 50, 'clip')
blockCtx.drawImage(img, 0, 0, width, height)

// 提取图案滑块并放到最左边
const y1 = 50 - r * 2 - 1
const ImageData = blockCtx.getImageData(xRef.value - 3, y1, L, L)
// 调整滑块画布宽度
blockRef.value.width = L
blockCtx.putImageData(ImageData, 0, y1)

上面的代码我们用到了 getImageDataputImageData,这两个 api 主要用来获取 canvas 画布场景像素数据和对场景进行像素数据的写入。实现后 的效果如下:

image.png

实现滑块移动和验证逻辑

实现滑块移动的方案也比较简单,我们只需要利用鼠标的 event 事件即可:

  • onMouseDown
  • onMouseMove
  • onMouseUp

image.png

以上是一个简单的示意图,具体实现代码如下:

const handleDragMove = (e) => {
    if (!isMouseDownRef.value) return false
    e.preventDefault()
    // 为了支持移动端, 可以使用e.touches[0]
    const eventX = e.clientX || e.touches[0].clientX
    const eventY = e.clientY || e.touches[0].clientY
    const moveX = eventX - originXRef.value
    const moveY = eventY - originYRef.value
    if (moveX < 0 || moveX + 36 >= width) return false
    sliderLeft.value = moveX;
    const blockLeft = (width - l - 2r) / (width - l) * moveX
    blockRef.value.style.left = blockLeft + 'px'
}

当然我们还需要对拖拽停止后的事件做监听,来判断是否验证成功,并埋入成功和失败的回调。

我们发现 vue3 版本的实现和我之前的 react 版本的方式类似, 主要区别就在于 api 用法和定义上, 这里分享一下具体的变量定义:

const {
  text,
  l,
  r,
  imgUrl,
  width,
  height,
  visible,
  onBeforeRefresh,
  onRefresh,
  onFail,
  onSuccess,
  onCustomVertify,
  onDraw,
} = props;

const isLoading = ref(false);
const sliderLeft = ref(0);
const sliderClass = ref("sliderContainer");
const textTip = ref(text);
const canvasRef = ref<any>(null);
const blockRef = ref<any>(null);
const imgRef = ref<any>(null);
const isMouseDownRef = ref<boolean>(false);
const trailRef = ref<number[]>([]);
const originXRef = ref<number>(0);
const originYRef = ref<number>(0);
const xRef = ref<number>(0);
const yRef = ref<number>(0);
const PI = Math.PI;
const L = l + r * 2 + 3; // 滑块实际边长

完整的实现效果如下:

2022-11-21 19.11.54.gif

将实现的滑动验证码组件发布到 npm 上

发布流程我在 从零开发一款轻量级滑动验证码插件 有详细的介绍, 接下来给大家展示一下通过 vite 打包组件包的配置:

export default defineConfig({
  ...baseConfig,
  build: {
    outDir: 'dist',
    lib: {
      entry: resolve(__dirname, '../packages/index.ts'),
      name: 'vue-slider-vertify',
      fileName: (format) => `vue-slider-vertify.${format}.js`,
    },
    rollupOptions: {
      // 确保外部化处理不想打包进库的依赖
      external: ['vue'],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue'
        }
      }
    }
  },
  plugins: [
    ...(baseConfig as any).plugins,
    dts(),
  ]
});

打包后的 dist 目录结构如下:

image.png

如果大家想了解详细的源码和打包过程, 可以在 github 上查看完整代码, 地址如下:

github.com/MrXujiang/v…

如果大家想通过 npm 的方式直接使用, 可以用我已经发布到 npm 服务器的 react-slider-vertify, 按照如下方式安装和使用:

# 或者 npm add @alex_xu/react-slider-vertify
yarn add @alex_xu/react-slider-vertify

在代码里使用:

import React from 'react';
import { Vertify } from '@alex_xu/react-slider-vertify';

export default () => {
    return <Vertify 
            width={320}
            height={160}
            onSuccess={() => alert('success')} 
            onFail={() => alert('fail')} 
            onRefresh={() => alert('refresh')} 
        />
};

image.png

扩展

大家也可以基于自己的业务需求改造我们的验证码, 实现更负复杂的功能, 改造成多种形态的验证码, 也欢迎大家随时贡献, 一起打造非常有意思的验证码组件。

后期规划

后面会继续围绕图形可视化来实现更多有意思的应用, 比如3D可视化, 图形编辑器, 可视化图表等, 如果大家感兴趣, 可以参考我的github: gitee.com/lowcode-chi…

如果文章对你有帮助, 欢迎点赞评论, 让我们一起探索真正的前端技术。

更多推荐

(10月最新) 前端图形学实战: 从零开发几何画板(vue3 + vite版)

Dooring无代码搭建平台技术演进之路

从零开发一款可视化大屏制作平台

从零开发一款图片编辑器Mitu