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

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

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. 安装

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

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

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

技术实现

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

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

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

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

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

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

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

  • 高性能

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

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

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

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

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

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

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

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

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

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

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

  • 镂空效果的 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() 裁切路径

实现方法如下:

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

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

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

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

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

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

  • onMouseDown

  • onMouseMove

  • onMouseUp

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

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

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

const handleDragEnd = (e) => {if (!isMouseDownRef.current) return falseisMouseDownRef.current = falseconst eventX = e.clientX || e.changedTouches[0].clientXif (eventX === originXRef.current) return falsesetSliderClass('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. 安装

$ 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 搭建的组件目录结构图:

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

通过这种方式 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 后的效果:

最后

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

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

更多推荐

  • 如何设计可视化搭建平台的组件商店?

  • 从零设计可视化大屏搭建引擎

  • 从零使用electron搭建桌面端可视化编辑器Dooring

  • (低代码)可视化搭建平台数据源设计剖析

  • 深度剖析github上15.1k Star项目:redux-thunk

  • 【H5制作】5分钟教你用H5-Dooring快速制作H5!

点个在看你最好看

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

  1. 从零破解一款轻量级滑动验证码

    关注下方「前端热榜」,回复 "思维图" 获取公众号所有JS思维图 昨天在掘金看到推荐文章<从零开发一款轻量级滑动验证码插件>[1],介绍了一些相关验证码及框架开发的知识 ...

  2. 手把手教你5分钟从零开发一款简易的IDEA插件!项目经验/毕设不愁了!

    我这个人没事就喜欢推荐一些好用的 IDEA 插件给大家.这些插件极大程度上提高了我们的生产效率以及编码舒适度. 不知道大家有没有想过自己开发一款 IDEA 插件呢? 我自己想过,但是没去尝试过.刚好有 ...

  3. 手牵手,使用uni-app从零开发一款视频小程序 (系列上 准备工作篇)

    系列文章 手牵手,使用uni-app从零开发一款视频小程序 (系列上 准备工作篇) 手牵手,使用uni-app从零开发一款视频小程序 (系列下 开发实战篇) 前言 好久不见,很久没更新博客了,前段时间 ...

  4. 从零开发一款笔记APP——神马笔记WhatsNote

    从零开发一款笔记APP--神马笔记WhatsNote 一.主要功能 二.开发过程 三.优质的笔记应用 四.附录 一.主要功能 笔记的主要功能分为三个部分: 管理 目录--多层目录结构 标签--单层结构 ...

  5. Android Camera:从零开发一款相机APP

    从零开发一款相机APP Day 1: 前言 一.Android Camera开发前景: 1)camera相关应用的领域 2)相关岗位介绍: 3)市场招聘介绍: 4)发展前景介绍: 二.学习这门课的重要 ...

  6. 从零开发一款相机 第五篇:Camera api1实现预览、拍照、录像功能

    本课程内容由 @小驰笔记 出品,欢迎关注,获取更多交流信息~ 欢迎访问个人博客:www.xiaochibiji.com 这节课,我们主要讲解如果使用camera api1接口,实现预览.拍照以及录像功 ...

  7. 个人从零开发一款 Android 应用、上线并盈利 | 项目复盘

    最近个人开发的一款应用 言叶 刚刚发布了 1.4.0,至此,我想要开发的大部分功能已经完成了.本来我也想做一次复盘,刚好趁这个机会分析下并发出来.在这篇文章中,我想分析的并不仅仅是技术,除此此外,我也 ...

  8. 从“零”开发一款知识图谱应用产品

    课程介绍 本达人课展现了从"零"开始完成一个知识图谱应用产品开发过程的完整实录. 这里的"零",意味着对读者的"零 Python 编程语言基础&quo ...

  9. Android Camera:从零开发一款相机APP Day01:前景

    一.Android Camera开发前景: 1)camera相关应用的领域2)相关岗位介绍:3)市场招聘介绍:4)发展前景介绍: 二.学习这门课的重要性: 1)适合的人群:2)熟悉和了解Android ...

最新文章

  1. 一文读懂 Nginx
  2. 今日头条算法原理(全文)【转】
  3. 如何在群晖服务器上启用plex远程访问,如何在Synology NAS上更新Plex | MOS86
  4. 这个工具可以快速查看文章引用、获取全文、研究者状态
  5. 江阴市高中计算机会考知识点,最新!2019年江阴中考体育考试方案发布!速看.........
  6. jQuery按钮隐藏与显示
  7. 【华为云技术分享】《跟唐老师学习云网络》 - TUN/TAP网线
  8. 网络发展之网络电话应用
  9. java多线程编码实现_Java多线程编码
  10. oracle共享内存系统全局,Oracle10g 管理系统全局区简介
  11. 「3D手指血管扫描」一套无法复制的生物识别系统
  12. mysql number 类型_MySQL 数据类型(转)
  13. idea vscode快捷键
  14. 人工智能导论测试题——第3章自动推理与专家系统
  15. 计算机主机有自带的声音吗,Windows XP 系统中没有音频设备,怎么办?
  16. ChromeOS 体验
  17. 基于JavaWeb的背单词系统的设计与实现
  18. 【python算法】算法之线性增长与二次方增长小实验举例
  19. 锦城学院计算机系考研,考研心得分享
  20. 【模电笔记】第一章3、4晶体三极管、场效应管

热门文章

  1. 盛世昊通董车长2.0“后“积薄发,点爆汽车后市场
  2. 微信小程序 扫码 加载图片
  3. Shopee店铺没有流量?3步教你如何诊断店铺
  4. 强化学习、GAN与多巴胺对撞:阿里AI 智能体认知研讨会干货
  5. 不同的打法,相同的内核,BAT车联网谁也不比谁更强
  6. Android 使用MediaPlayer播放音频详解
  7. 全球与中国2,5-二甲基吡啶试剂市场现状及未来发展趋势
  8. PHP 对接美团大众点评团购券的开发步骤
  9. 20180814 实习小结关于前端
  10. python里range什么意思_python里range什么意思