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

关注并将「趣谈前端」设为星标

每天定时分享技术干货/优秀开源/技术思维

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

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

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

  • 前端组件设计的基本思路和技巧

  • canvas 基本知识和使用

  • react hooks 基本知识和使用

  • 滑动验证码基本设计原理

  • 如何封装一款可扩展的滑动验证码组件

  • 如何使用 dumi 搭建组件文档

  • 如何发布自己第一个npm组件包

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

效果演示

304df8d1b3872ae767c42b491d5ce331.gif

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

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

基本使用

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

  1. 安装

# 或者 yarn add @alex_xu/react-slider-vertify
npm i @alex_xu/react-slider-vertify -S
  1. 使用

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')} 
        />
};

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

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

c892a747145c7dac784930e915e8f66a.png

技术实现

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

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

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

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

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

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

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

  • 高性能

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

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

cc59189456f7a5b6c1ce884abdfe5b4c.png

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

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

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

b1dfdb5fc1f81574ce048946580c1514.png

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

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

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

d28e426afe0217361baaf2d0d2b4b8bb.png

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

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

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

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 样式之后我们看到的界面是这样的:

f4cb9b9cd1d3ba5132f1066a41203ddd.png

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

  • 镂空效果的 canvas 图片实现

  • 镂空图案 canvas 实现

  • 滑块移动和验证逻辑实现

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

f4ba2c15f0506cabca63f5ed43693b29.png

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

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

27dad5a473713c52957900a5e62fa3d4.png

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

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

3efe05383cb939f227c44e0b996d6a9e.png

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

  • beginPath() 开始路径绘制

  • moveTo() 移动笔触到指定点

  • arc() 绘制弧形

  • lineTo() 画线

  • stroke() 描边

  • fill() 填充

  • clip() 裁切路径

实现方法如下:

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上有个形象的例子:

b914fa905c76fae2e19c653de47e3161.png

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

f8c714928c6ec2cfbbc36a826b7d84a5.png

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

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

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

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

2.实现镂空图案 canvas

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

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 画布场景像素数据和对场景进行像素数据的写入。实现后 的效果如下:

031909017dc85637c79c9e19b32f3e32.png

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

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

  • onMouseDown

  • onMouseMove

  • onMouseUp

8d0047e65eebed34a6716bb1f58a23d6.png

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

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'
}

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

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)
    }
}

实现后的效果如下:

5644be007706f1725b579d4fbe4c9047.gif

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

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

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

2b090613fb72732b7369c9c2b4145109.png 939699f10bea94d231ff57025d2d18a9.png

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

  1. 安装

$ 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. 本地运行

npm run dev
# or
yarn dev
  1. 编写文档

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

7002e597ed1f9e8e622d8b384a766c00.png

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

1ef47e41846a09a8d3a7c37551c0e173.png

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

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

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

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

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

npm login

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

npm publish --access public

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

{
    "scripts": {
        "start": "dumi dev",
        "release": "npm run build && npm publish --access public",
  }
}

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

a523502db804d7b30c02fd3f9f31a1fe.png

最后

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

github: react-slider-vertify
推荐:lowcode可视化社区
公众号: 趣谈前端

更多推荐

点个在看你最好看

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Android开发中,滑动返回上一级是一种常见的页面切换效果。可以通过以下步骤来实现: 1. 在布局文件中添加一个ViewPager控件,用于显示页面内容。 2. 在Activity中使用ViewPager控件,并设置ViewPager的PageTransformer为一个自定义的Transformer,用于实现滑动返回上一级的效果。 3. 在自定义的Transformer中,根据手指滑动的距离和方向,计算出当前页面的偏移量,并将当前页面和前一页的偏移量进行动态调整。 下面是一个示例代码: 在布局文件中添加ViewPager控件: ```xml <androidx.viewpager.widget.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 在Activity中添加如下代码: ```java public class MainActivity extends AppCompatActivity { private ViewPager mViewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewPager = findViewById(R.id.view_pager); // 设置ViewPager的PageTransformer mViewPager.setPageTransformer(true, new MyPageTransformer()); } // 自定义的PageTransformer private class MyPageTransformer implements ViewPager.PageTransformer { private static final float MIN_SCALE = 0.75f; @Override public void transformPage(@NonNull View view, float position) { int pageWidth = view.getWidth(); if (position < -1) { // 左侧页面 view.setAlpha(0f); } else if (position <= 0) { // 当前页面 view.setAlpha(1f); view.setTranslationX(0f); view.setScaleX(1f); view.setScaleY(1f); } else if (position <= 1) { // 下一个页面 view.setAlpha(1f - position); view.setTranslationX(-pageWidth * position); float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position)); view.setScaleX(scaleFactor); view.setScaleY(scaleFactor); } else { // 右侧页面 view.setAlpha(0f); } } } } ``` 以上代码实现了在用户滑动页面时实现滑动返回上一级的效果。在自定义的PageTransformer中,根据用户手指滑动的距离和方向,计算出当前页面的偏移量,并将当前页面和前一页的偏移量进行动态调整。需要注意的是,这种实现方式需要在ViewPager中至少包含两个页面,否则滑动返回上一级没有效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值