从零开发一款轻量级滑动验证码插件(深度复盘)

2022-02-09 14:20:03 浏览数 (1)

github地址: https://github.com/MrXujiang/react-slider-vertify

之前一直在分享 低代码可视化 的文章,其中涉及到很多有意思的知识点和设计思想,今天继续和大家分享一款非常有趣且实用的前端实战项目——从零基于 react canvas 实现一个滑动验证码,并将其发布到 npm 上供他人使用。当然如果大家更喜欢 vue 的开发方式,也不用担心,文中的设计思想和思路都是通用的,如果大家想学习如何封装 vue 组件并发布到 npm 上,也可以参考我之前的文章: 从零到一教你基于vue开发一个组件库

从这个实战项目中我们可以学到如下知识点:

  • 前端组件设计的基本思路和技巧
  • canvas 基本知识和使用
  • react hooks 基本知识和使用
  • 滑动验证码基本设计原理
  • 如何封装一款可扩展的滑动验证码组件
  • 如何使用 dumi 搭建组件文档
  • 如何发布自己第一个npm组件包

如果你对以上任意知识点感兴趣,相信这篇文章都会给你带来启发。

效果演示

滑动验证组件基本使用和技术实现

上图是实现的滑动验证组件的一个效果演示,当然还有很多配置项可以选择,以便支持更多 定制化 的场景。接下来我先介绍一下如何安装和使用这款验证码插件,让大家有一个直观的体验,然后我会详细介绍一下滑动验证码的实现思路,如果大家有一定的技术基础,也可以直接跳到技术实现部分。

基本使用

因为 react-slider-vertify 这款组件我已经发布到 npm 上了,所以大家可以按照如下方式安装和使用:

  1. 安装
代码语言:javascript复制
# 或者 yarn add @alex_xu/react-slider-vertify
npm i @alex_xu/react-slider-vertify -S
  1. 使用
代码语言:javascript复制
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')} 
        />
};

通过以上两步我们就可以轻松使用这款滑动验证码组件了,是不是很简单?

当然我也暴露了很多可配置的属性,让大家对组件有更好的控制。参考如下:

技术实现

在做这个项目之前我也研究了一些滑动验证码的知识以及已有的技术方案,收获很多。接下来我会以我的组件设计思路来和大家介绍如何用 react 来实现和封装滑动验证码组件,如果大家有更好的想法和建议, 也可以在评论区随时和我反馈。

1.组件设计的思路和技巧

每个人都有自己设计组件的方式和风格,但最终目的都是更 优雅 的设计组件。这里我大致列举一下 优雅 组件的设计指标:

  • 可读性(代码格式统一清晰,注释完整,代码结构层次分明,编程范式使用得当)
  • 可用性(代码功能完整,在不同场景都能很好兼容,业务逻辑覆盖率)
  • 复用性(代码可以很好的被其他业务模块复用)
  • 可维护性(代码易于维护和扩展,并有一定的向下/向上兼容性)
  • 高性能

以上是我自己设计组件的考量指标,大家可以参考一下。

另外设计组件之前我们还需要明确需求,就拿滑动验证码组件举例,我们需要先知道它的使用场景(用于登录注册、活动、论坛、短信等高风险业务场景的人机验证服务)和需求(交互逻辑,以什么样的方式验证,需要暴露哪些属性)。

以上就是我梳理的一个大致的组件开发需求,在开发具体组件之前,如果遇到复杂的业务逻辑,我们还可以将每一个实现步骤列举出来,然后一一实现,这样有助于整理我们的思路和更高效的开发。

2.滑动验证码基本实现原理

在介绍完组件设计思路和需求分析之后,我们来看看滑动验证码的实现原理。

我们都知道设计验证码的主要目的是为了防止机器非法暴力地入侵我们的应用,其中核心要解决的问题就是判断应用是谁在操作( or 机器),所以通常的解决方案就是随机识别

上图我们可以看到只有用户手动将滑块拖拽到对应的镂空区域,才算验证成功,镂空区域的位置是随机的(随机性测试这里暂时以前端的方式来实现,更安全的做法是通过后端来返回位置和图片)。

基于以上分析我们就可以得出一个基本的滑动验证码设计原理图:

接下来我们就一起封装这款可扩展的滑动验证码组件。

3.封装一款可扩展的滑动验证码组件

按照我开发组件一贯的风格,我会先基于需求来编写组件的基本框架:

代码语言:javascript复制
import React, { useRef, useState, useEffect, ReactNode } from 'react';

interface IVertifyProp {
    /**
     * @description   canvas宽度  
     * @default       320
     */
    width:number, 
    /**
     * @description   canvas高度  
     * @default       160
     */
    height:number, 
    /**
     * @description   滑块边长  
     * @default       42
     */
     l:number,
     /**
     * @description   滑块半径 
     * @default       9
     */
      r:number,
     /**
     * @description   是否可见
     * @default       true
     */
      visible:boolean,
     /**
     * @description   滑块文本
     * @default       向右滑动填充拼图
     */
      text:string | ReactNode,
      /**
     * @description   刷新按钮icon, 为icon的url地址
     * @default       -
     */
       refreshIcon:string,
     /**
     * @description   用于获取随机图片的url地址
     * @default       https://picsum.photos/${id}/${width}/${height}, 具体参考https://picsum.photos/, 只需要实现类似接口即可
     */
       imgUrl:string,
    /**
     * @description   验证成功回调  
     * @default       ():void => {}
     */
    onSuccess:VoidFunction, 
    /**
     * @description   验证失败回调  
     * @default       ():void => {}
     */
    onFail:VoidFunction, 
    /**
     * @description   刷新时回调  
     * @default       ():void => {}
     */
    onRefresh:VoidFunction
}

export default ({ 
    width = 320,
    height = 160,
    l = 42,
    r = 9,
    imgUrl,
    text,
    refreshIcon = 'http://yourimgsite/icon.png',
    visible = true,
    onSuccess,
    onFail,
    onRefresh
 }: IVertifyProp) => {
     return <div className="vertifyWrap">
        <div className="canvasArea">
            <canvas width={width} height={height}></canvas>
            <canvas className="block" width={width} height={height}></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" onClick={handleRefresh}></div>
        <div className="loadingContainer">
            <div className="loadingIcon"></div>
            <span>加载中...</span>
        </div>
    </div>
 }

以上就是我们组件的基本框架结构。从代码中可以发现组件属性一目了然,这都是提前做好需求整理带来的好处,它可以让我们在编写组件时思路更清晰。在编写好基本的 css 样式之后我们看到的界面是这样的:

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

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

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

因为组件实现完全采用的 react hooks ,如果大家对 hooks 不熟悉也可以参考我之前的文章:

  • 10分钟教你手写8个常用的自定义hooks

1.实现镂空效果的 canvas 图片

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

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

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

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

实现方法如下:

代码语言:javascript复制
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)
    // anticlockwise为一个布尔值。为true时,是逆时针方向,否则顺时针方向
    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.8)'
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'
    ctx.stroke()
    ctx.globalCompositeOperation = 'destination-over'
    // 判断是填充还是裁切, 裁切主要用于生成图案滑块
    operation === 'fill'? ctx.fill() : ctx.clip()
}

这块实现方案也是参考了 yield 大佬的原生 js 实现,这里需要补充的一点是 canvasglobalCompositeOperation 属性,它的主要目的是设置如何将一个源(新的)图像绘制到目标(已有)的图像上。

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

w3c上有个形象的例子:

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

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

代码语言:javascript复制
const canvasCtx = canvasRef.current.getContext('2d')
// 绘制镂空形状
drawPath(canvasCtx, 50, 50, 'fill')

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

当然至于如何生成随机图片和随机位置,实现方式也很简单,前端实现的话采用 Math.random 即可。

2.实现镂空图案 canvas

上面实现了镂空形状,那么镂空图案也类似,我们只需要使用 clip() 方法将图片裁切到形状遮罩里,并将镂空图案置于画布左边即可。代码如下:

代码语言:javascript复制
const blockCtx = blockRef.current.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.current - 3, y1, L, L)
// 调整滑块画布宽度
blockRef.current.width = L
blockCtx.putImageData(ImageData, 0, y1)

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

3.实现滑块移动和验证逻辑

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

  • onMouseDown
  • onMouseMove
  • onMouseUp

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

代码语言:javascript复制
const handleDragMove = (e) => {
    if (!isMouseDownRef.current) 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.current
    const moveY = eventY - originYRef.current
    if (moveX < 0 || moveX   36 >= width) return false
    setSliderLeft(moveX)
    const blockLeft = (width - l - 2r) / (width - l) * moveX
    blockRef.current.style.left = blockLeft   'px'
}

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

代码语言:javascript复制
const handleDragEnd = (e) => {
    if (!isMouseDownRef.current) return false
    isMouseDownRef.current = false
    const eventX = e.clientX || e.changedTouches[0].clientX
    if (eventX === originXRef.current) return false
    setSliderClass('sliderContainer')
    const { flag, result } = verify()
    if (flag) {
      if (result) {
        setSliderClass('sliderContainer sliderContainer_success')
        // 成功后的自定义回调函数
        typeof onSuccess === 'function' && onSuccess()
      } else {
        // 验证失败, 刷新重置
        setSliderClass('sliderContainer sliderContainer_fail')
        setTextTip('请再试一次') 
        reset()
      }
    } else {
      setSliderClass('sliderContainer sliderContainer_fail')
      // 失败后的自定义回调函数
      typeof onFail === 'function' && onFail()
      setTimeout(reset.bind(this), 1000)
    }
}

实现后的效果如下:

当然还有一些细节需要优化处理,这里在 github 上有完整的代码,大家可以参考学习一下,如果大家想对该组件参与贡献,也可以随时提 issue

4.如何使用 dumi 搭建组件文档

为了让组件能被其他人更好的理解和使用,我们可以搭建组件文档。作为一名热爱开源的前端 coder,编写组件文档也是个很好的开发习惯。接下来我们也为 react-slider-vertify 编写一下组件文档,这里我使用 dumi 来搭建组件文档,当然大家也可以用其他方案(比如storybook)。我们先看一下搭建后的效果:

dumi 搭建组件文档非常简单,接下来和大家介绍一下安装使用方式。

  1. 安装
代码语言:javascript复制
$ npx @umijs/create-dumi-lib        # 初始化一个文档模式的组件库开发脚手架
# or
$ yarn create @umijs/dumi-lib

$ npx @umijs/create-dumi-lib --site # 初始化一个站点模式的组件库开发脚手架
# or
$ yarn create @umijs/dumi-lib --site
  1. 本地运行
代码语言:javascript复制
npm run dev
# or
yarn dev
  1. 编写文档

dumi 约定式的定义了文档编写的位置和方式,其官网上也有具体的饭介绍,这里简单给大家上一个 dumi 搭建的组件目录结构图:

我们可以在 docs 下编写组件库文档首页和引导页的说明,在单个组件的文件夹下使用 index.md 来编写组件自身的使用文档,当然整个过程非常简单,我这里举一个文档的例子:

通过这种方式 dumi 就可以帮我们自动渲染一个组件使用文档。如果大家想学习更多组件文档搭建的内容,也可以在 dumi 官网学习。

5.发布自己第一个npm组件包

最后一个问题就是组件发布。之前很多朋友问我如何将自己的组件发布到 npm 上让更多人使用,这块的知识网上有很多资料可以学习,那今天就以滑动验证码 @alex_xu/react-slider-vertify 的例子,来和大家做一个简单的介绍。

  1. 拥有一个 npm 账号并登录

如果大家之前没有 npm 账号,可以在 npm 官网 注册一个,然后用我们熟悉的 IDE 终端登录一次:

代码语言:javascript复制
npm login

跟着提示输入完用户名密码之后我们就能通过命令行发布组件包了:

代码语言:javascript复制
npm publish --access public

之所以指令后面会加 public 参数,是为了避免权限问题导致组件包无法发布成功。我们为了省事也可以把发布命令配置到 package.json 中,在组件打包完成后自动发布:

代码语言:javascript复制
{
    "scripts": {
        "start": "dumi dev",
        "release": "npm run build && npm publish --access public",
  }
}

这样我们就能将组件轻松发布到 npm 上供他人使用啦! 我之前也开源了很多组件库,如果大家对组件打包细节和构建流程有疑问,也可以参考我之前开源项目的方案。发布到 npm 后的效果:

最后

如果大家对可视化搭建或者低代码/零代码感兴趣,也可以参考我往期的文章或者在评论区交流你的想法和心得,欢迎一起探索前端真正的技术。

0 人点赞