從零開發一款輕量級滑動驗證碼插件(深度復盤)
之前一直在分享 低代碼 和 可視化 的文章,其中涉及到很多有意思的知識點和設計思想,今天繼續和大家分享一款非常有趣且實用的前端實戰項目——從零基于 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
2.使用
- 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">→</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
一.實現鏤空效果的 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 = 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 實現,這里需要補充的一點是 canvas 的 globalCompositeOperation 屬性,它的主要目的是設置如何將一個源(新的)圖像繪制到目標(已有)的圖像上。
- 源圖像 = 我們打算放置到畫布上的繪圖
- 目標圖像 = 我們已經放置在畫布上的繪圖
w3c上有個形象的例子:
這里之所以設置該屬性是為了讓鏤空的形狀不受背景底圖的影響并覆蓋在背景底圖的上方。如下:
接下來我們只需要將圖片繪制到畫布上即可:
- const canvasCtx = canvasRef.current.getContext('2d')
- // 繪制鏤空形狀
- drawPath(canvasCtx, 50, 50, 'fill')
- // 畫入圖片
- canvasCtx.drawImage(img, 0, 0, width, height)
當然至于如何生成隨機圖片和隨機位置,實現方式也很簡單,前端實現的話采用 Math.random 即可。
二.實現鏤空圖案 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)
上面的代碼我們用到了 getImageData 和 putImageData,這兩個 api 主要用來獲取 canvas 畫布場景像素數據和對場景進行像素數據的寫入。實現后 的效果如下:
三.實現滑塊移動和驗證邏輯
實現滑塊移動的方案也比較簡單,我們只需要利用鼠標的 event 事件即可:
- onMouseDown
- onMouseMove
- onMouseUp
以上是一個簡單的示意圖,具體實現代碼如下:
- 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)
- }
- }
實現后的效果如下:
當然還有一些細節需要優化處理,這里在 github 上有完整的代碼,大家可以參考學習一下,如果大家想對該組件參與貢獻,也可以隨時提 issue。
四.如何使用 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
2.本地運行
- npm run dev
- # or
- yarn dev
3.編寫文檔
dumi 約定式的定義了文檔編寫的位置和方式,其官網上也有具體的飯介紹,這里簡單給大家上一個 dumi 搭建的組件目錄結構圖:
我們可以在 docs 下編寫組件庫文檔首頁和引導頁的說明,在單個組件的文件夾下使用 index.md 來編寫組件自身的使用文檔,當然整個過程非常簡單,我這里舉一個文檔的例子:
通過這種方式 dumi 就可以幫我們自動渲染一個組件使用文檔。如果大家想學習更多組件文檔搭建的內容,也可以在 dumi 官網學習。
五.發布自己第一個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 后的效果:
本文轉載自微信公眾號「趣談前端」