基于React封裝一個驗證碼輸入控件
引言
郵箱、手機驗證碼輸入是許多在線服務(wù)和網(wǎng)站常見的安全驗證方式之一。這種方式通常用于確保用戶在進行敏感操作(例如注冊、修改密碼、重置密碼等)時的身份驗證。
最近在做項目剛好有驗證碼相關(guān)的需求, 本著不重復(fù)造輪子的原則, 一頓 Google 試圖找到一個現(xiàn)成的組件, 奈何找了一圈都沒找到滿意的, 要么就是交互感覺不太合理、要么就是基本停止維護了的!!
最后沒辦法就自己造一個了, 這里主要參考了 react-auth-code-input, 而本文則是整個思路開發(fā)流程的記錄!!
DEMO 演示可查閱: blog/auth-codes
本文完整源碼可查閱: coding/blog/AuthCodes
一、需求描述
開始前我們先梳理下一般驗證碼輸入控件的常規(guī)需求有哪些:
- 假設(shè)我們驗證碼有
6位, 則我們需要有6個輸入框, 每個輸入框只允許輸入一位數(shù)字(這里假設(shè)驗證碼都是數(shù)字組成) - 在輸入驗證碼過程中, 可連續(xù)進行輸入、刪除等操作
- 支持黏貼復(fù)制的內(nèi)容
- ...
二、布局
在開始前我們先來完成基本的布局, 如下代碼所示:
- 聲明狀態(tài)
codes用于存儲每個驗證碼, 也就是每個輸入框的值, 這里我將codes設(shè)置為一個數(shù)組, 方便后面修改每個位置的驗證碼 - 假設(shè)我們驗證碼長度為
6位, 所以這里我為codes默認值了一個長度為6的數(shù)組, 數(shù)組每個初始值為空字符串 - 然后我們通過
codes.map渲染出所有輸入框 - 最后我們還聲明了
inputsRef來存儲所有輸入框的DOM節(jié)點, 我們后面需要通過它來調(diào)用原生DOM的API
import React, { useState, useRef } from 'react';
import scss from './com.module.scss';
export default () => {
const [codes, setCodes] = useState(Array.from({ length: 6 }, () => ''));
const inputsRef = useRef([]);
return (
<div>
{codes.map((value, index) => (
<input
type="text"
key={index}
value={value}
maxLength={1}
className={scss.input}
ref={(ele) => (inputsRef.current[index] = ele)}
/>
))}
</div>
);
};
這里我們對輸入框設(shè)置了一些基本的樣式
.input {
width: 40px;
margin: 10px;
font-size: 18px;
line-height: 40px;
text-align: center;
border-radius: 4px;
border: 1px solid #d9d9d9;
}
到此頁面的基本效果如下:

三、動態(tài)綁定(處理 onChange 事件)
上文只是完成了基本的布局, 并且輸入框 value 和狀態(tài) codes 內(nèi)的值綁定在了一起, 這里輸入框輸入值會發(fā)現(xiàn)并沒有生效, 那是因為狀態(tài) codes 沒有被修改!
下面我們?yōu)檩斎肟蛟O(shè)置 onChange 事件, 在輸入框輸入值時動態(tài)的修改狀態(tài) codes 中對應(yīng)位置的值!!
下面是 onChange 事件的處理函數(shù):
- 兩個參數(shù),
index和event, 正如命名所示,index對應(yīng)輸入框索位置,event則是輸入對應(yīng)的change事件對象, 通過它來獲取輸入值 - 特別說明, 本文驗證碼都是數(shù)字, 所以在函數(shù)內(nèi)部還需要針對輸入內(nèi)容進行校驗, 只允許輸入數(shù)字
0~9 - 函數(shù)內(nèi)還有一個特殊處理邏輯, 就是當我們輸入有效值后, 需要將鼠標光標聚焦到下一個輸入框, 如此用戶就可以連續(xù)進行輸入了, 至于實現(xiàn)方法很簡單, 這里直接調(diào)用
inputsRef中對應(yīng)輸入框DOM節(jié)點的focus方法即可 - 最后我們調(diào)用,
setCodes修改狀態(tài)codes, 這樣輸入框的值才能動態(tài)的修改
const handleChange = useCallback((index, event) => {
const currentValue = event.target.value.match(/[0-9]{1}/)
? event.target.value
: '';
// 如果輸入有效值, 則自動聚焦到下一個輸入框
if (currentValue) {
inputsRef.current[index + 1]?.focus();
}
setCodes((pre) => {
const newData = [...pre];
newData[index] = currentValue;
return newData;
});
}, []);
最為為每個輸入框綁定 onChange 事件, 主要這里使用了 bind 來綁定 index:
<div>
{codes.map((value, index) => (
<input
...
+ onChange={handleChange.bind(null, index)}
ref={(ele) => (inputsRef.current[index] = ele)}
/>
))}
</div>
最后效果如下: 輸入驗證碼, 光標自動跳轉(zhuǎn)到下一個輸入框

四、刪除處理
上文我們完成驗證碼的輸入, 但是在輸入過程中, 難免會輸入錯誤的數(shù)字, 所以就需要實現(xiàn)刪除驗證碼的能力, 需求如下:
- 當我們按下
刪除鍵 - 如果當前輸入框有值, 則刪除當前輸入框中的內(nèi)容
- 如果當前輸入框沒有值, 則刪除上一個輸入框內(nèi)容, 并且聚焦到上一個輸入框
需求其實已經(jīng)很明確了, 我只需要通過 onKeyDown 來監(jiān)聽鍵盤按下事件, 從而判斷用戶是否按下 刪除鍵, 如果按下 刪除鍵 則按照需求邏輯進行編碼即可, 具體代碼如下:
- 函數(shù)接收兩個參數(shù)
index和event,index表示當前光標所在的輸入框索引位置,event則是事件對象 - 通過
event.key的值來確定是否按下刪除鍵(Backspace), 如果不是, 則不進行任何處理 - 剩下就按需求來, 如果當前輸入框有值則清除當前輸入框內(nèi)容
- 如果當前輸入框沒值, 則清除上一個輸入框內(nèi)容, 并且將光標移到上一個輸入框中, 這里還需要考慮下邊界情況, 如果當前輸入框已經(jīng)是第一個了, 就無需進行任何處理
const handleDelete = useCallback((index, event) => {
const { key } = event;
// 是否按下刪除鍵, 否提前結(jié)束
if (key !== 'Backspace') {
return;
}
// 1. 如果當前輸入框有值, 則刪除當前輸入框內(nèi)容
if (codes[index]) {
setCodes((pre) => {
const newData = [...pre];
newData[index] = '';
return newData;
});
} else if (index > 0) {
// 2. 如果當前輸入框沒有值(考慮下邊界的情況 index === 0): 則刪除上一個輸入框內(nèi)容, 并且光標聚焦到上一個輸入框
setCodes((pre) => {
const newData = [...pre];
newData[index - 1] = '';
return newData;
});
inputsRef.current[index - 1].focus();
}
}, [codes]);
最后為每個輸入框綁定 onKeyDown 事件, 主要這里使用了 bind 來綁定 index:
<div>
{codes.map((value, index) => (
<input
...
+ onKeyDown={handleDelete.bind(null, index)}
onChange={handleChange.bind(null, index)}
ref={(ele) => (inputsRef.current[index] = ele)}
/>
))}
</div>
最后效果如下: 輸入驗證碼后, 按下刪除鍵, 能夠連續(xù)刪除驗證碼內(nèi)容

五、粘貼處理
在大部分情況下, 我們都是直接復(fù)制驗證碼然后直接黏貼使用, 所以我們接下來來實現(xiàn)的功能就是:
- 允許在任意輸入框黏貼數(shù)據(jù)
- 自動將剪切板的數(shù)字回填到輸入框中
- 這里不做過多的處理, 不管光標在哪個位置, 都從第一個輸入框開始填充數(shù)字
- 注意的是, 這里光標還需要自動聚焦到最后一個輸入框內(nèi)容為空的位置
具體實現(xiàn)代碼如下:
- 通過
event.clipboardData.getData獲取到剪切板內(nèi)容 - 過濾掉剪切板中非數(shù)值部分內(nèi)容
- 生成新狀態(tài)
codes: 先創(chuàng)建了一長度為6的數(shù)組, 并使用剪切板的數(shù)字就行填充, 不夠的用空字符進行填充, 最后使用setCodes來修改狀態(tài)值 - 光標位置修改, 根據(jù)剪切板數(shù)字長度來進行計算
const handlePaste = useCallback((event) => {
const pastedValue = event.clipboardData.getData('Text'); // 讀取剪切板數(shù)據(jù)
const pastNum = pastedValue.replace(/[^0-9]/g, ''); // 去除數(shù)據(jù)中非數(shù)字部分, 只保留數(shù)字
// 重新生成 codes: 6 位, 每一位取剪切板對應(yīng)位置的數(shù)字, 沒有則置空
const newData = Array.from(
{ length: 6 },
(_, index) => pastNum.charAt(index) || '',
);
setCodes(newData); // 修改狀態(tài) codes
// 光標要聚焦的輸入框的索引, 這里取 pastNum.length 和 5 的最小值即可, 當索引為 5 就表示最后一個輸入框了
const focusIndex = Math.min(pastNum.length, 5);
inputsRef.current[focusIndex]?.focus();
}, []);
最后為每個輸入框綁定 onPaste(黏貼) 事件
<input
...
onPaste={handlePaste}
/>
最后效果如下: 光標聚焦在任意輸入框, 進行黏貼后, 即可自動用剪切板內(nèi)的數(shù)字來填充輸入框

六、第一階段完成
到此整體功能已經(jīng)差不多了, 下面是目前為止完整的代碼(刪除了 CSS 部分)
import React, { useState, useRef, useCallback } from 'react';
export default () => {
const [codes, setCodes] = useState(Array.from({ length: 6 }, () => ''));
const inputsRef = useRef([]);
const handleChange = useCallback((index, event) => {
const currentValue = event.target.value.match(/[0-9]{1}/)
? event.target.value
: '';
// 如果輸入有效值, 則自動聚焦到下一個輸入框
if (currentValue) {
inputsRef.current[index + 1]?.focus();
}
setCodes((pre) => {
const newData = [...pre];
newData[index] = currentValue;
return newData;
});
}, []);
const handleDelete = useCallback((index, event) => {
const { key } = event;
// 是否按下刪除鍵, 否提前結(jié)束
if (key !== 'Backspace') {
return;
}
// 1. 如果當前輸入框有值, 則刪除當前輸入框內(nèi)容
if (codes[index]) {
setCodes((pre) => {
const newData = [...pre];
newData[index] = '';
return newData;
});
} else if (index > 0) {
// 2. 如果當前輸入框沒有值(考慮下邊界的情況 index === 0): 則刪除上一個輸入框內(nèi)容, 并且光標聚焦到上一個輸入框
setCodes((pre) => {
const newData = [...pre];
newData[index - 1] = '';
return newData;
});
inputsRef.current[index - 1].focus();
}
}, [codes]);
const handlePaste = useCallback((event) => {
const pastedValue = event.clipboardData.getData('Text'); // 讀取剪切板數(shù)據(jù)
const pastNum = pastedValue.replace(/[^0-9]/g, ''); // 去除數(shù)據(jù)中非數(shù)字部分, 只保留數(shù)字
// 重新生成 codes: 6 位, 每一位取剪切板對應(yīng)位置的數(shù)字, 沒有則置空
const newData = Array.from(
{ length: 6 },
(_, index) => pastNum.charAt(index) || '',
);
setCodes(newData); // 修改狀態(tài) codes
// 光標要聚焦的輸入框的索引, 這里取 pastNum.length 和 5 的最小值即可, 當索引為 5 就表示最后一個輸入框了
const focusIndex = Math.min(pastNum.length, 5);
inputsRef.current[focusIndex]?.focus();
}, []);
return (
<div>
{codes.map((value, index) => (
<input
type="text"
key={index}
value={value}
maxLength={1}
onPaste={handlePaste}
onKeyDown={handleDelete.bind(null, index)}
onChange={handleChange.bind(null, index)}
ref={(ele) => (inputsRef.current[index] = ele)}
/>
))}
</div>
);
};
基本功能有了, 下面我們對組件進行簡單的封裝、優(yōu)化....
七、暴露 onChange 事件
這里我們希望父組件可以通過 onValueChange 來監(jiān)聽到內(nèi)部狀態(tài) codes 的變更, 做法就很簡單了:
- 抽離一個通過方法
resetCodes, 修改狀態(tài)的地方全部使用resetCodes方法 resetCodes方法內(nèi)部則是調(diào)用setCodes方法修改codes同時調(diào)用父組件傳進來的onValueChange方法resetCodes支持傳一個數(shù)組進來, 也可以是一個index一個value; 這么做的原因主要是為了支持不同場景下修改狀態(tài)codes的需求
// 修改狀態(tài) codes
const resetCodes = useCallback((index, value) => {
setCodes((pre) => {
let newData = [...pre];
if (Array.isArray(index)) {
newData = index;
}
if (typeof index === 'number') {
newData[index] = value;
}
onValueChange?.(newData.join(''));
return newData;
});
}, [onValueChange]);
最后還需要將代碼里調(diào)用 setCodes 的地方改為 resetCodes, 這里就不做演示了; 修改完成之后, 我們就可以通過 onValueChange 監(jiān)聽到組件內(nèi)部 codes 的變更了
<AuthCode onValueChange={(codes) => console.log(codes)} />
最后效果如下:

八、暴露 onComplete 事件
這里我們還希望在輸入完所有驗證碼后, 能夠被組件外部監(jiān)聽到, 這樣就可以直接拿到完整的驗證碼向后端服務(wù)發(fā)起校驗....
其實有了上面的基礎(chǔ), 我們可以直接在 resetCodes 中進行處理: 在修改狀態(tài) codes 前判斷下所有驗證碼是否都已經(jīng)輸入, 如果已全部輸入則調(diào)用父組件的 onComplete 事件
// 修改狀態(tài) codes
const resetCodes = useCallback((index, value) => {
setCodes((pre) => {
let newData = [...pre];
if (Array.isArray(index)) {
newData = index;
}
if (typeof index === 'number') {
newData[index] = value;
}
+ // 處理 onComplete
+ if (newData.every(Boolean) && onComplete) {
+ onComplete(newData.join(''));
+ }
onValueChange?.(newData.join(''));
return newData;
});
+ }, [onValueChange, onComplete]);
接下來我們就可以在驗證碼全部輸入后, 通過 onComplete 監(jiān)聽到
<AuthCode onComplete={(codes) => console.log(codes)} />
最后效果如下:

九、自動聚焦
這個需求就很簡單咯, 就是希望組件在初始化時可以將鼠標光標自動聚焦到第一個輸入框, 這樣用戶就可以直接進行輸入, 完成驗證碼的校驗!!!
實現(xiàn)方法就更簡單, 直接在 useEffect 中調(diào)用第一個輸入框的 DOM 節(jié)點的原生 focus 方法即可
useEffect(() => {
inputsRef.current[0].focus();
}, []);
十、聚焦時選中輸入框內(nèi)容
下面我們希望能夠在輸入框聚焦情況下, 能夠自動選中輸入框的內(nèi)容, 這樣的話就可以直接輸入內(nèi)容, 而不是先刪除再輸入內(nèi)容!!
實現(xiàn)方法很簡單:
- 通過
onFocus事件來實現(xiàn), 監(jiān)聽Focus(獲取焦點)事件 - 然后在事件處理函數(shù)內(nèi)調(diào)用事件
select方法來選中輸入框的內(nèi)容
const handleOnFocus = useCallback((e) => {
e.target.select();
}, []);
最后效果如下:

十一、暴露外面接口
最后我們希望父組件可以通過 ref 來獲取到一些組件內(nèi)部預(yù)設(shè)好的方法, 比如自動獲取焦點、清空所有輸入框內(nèi)容等等
如下代碼使用 forwardRef 配合 useImperativeHandle 完成 ref 的綁定
export default forwardRef((props, ref) => {
// ...
useImperativeHandle(ref, () => ({
// 獲取焦點
focus: (index = 0) => {
if (inputsRef.current) {
inputsRef.current[index].focus();
}
},
// 清空內(nèi)容
clear: () => {
resetCodes(codes.map(() => ''));
},
}));
// ...
}
調(diào)用方法如下所示:
export default () => {
const ref = useRef();
return (
<>
<Com ref={ref} />
<Button onClick={() => ref.current?.clear()}>
清空
</Button>
</>
);
};
最后效果如下:

十二、后續(xù)
到此基本差不多了, 剩下更多的可能是組件的封裝上的事情, 比如:
- 允許設(shè)置默認值
- 支持雙向綁定
- 支持設(shè)置驗證碼長度
- 支持設(shè)置驗證碼規(guī)則(純數(shù)字、純字母、字母數(shù)字混合)
- 支持設(shè)置
input參數(shù)(比如placeholder等等) - ...
以上就是基于React封裝一個驗證碼輸入控件的詳細內(nèi)容,更多關(guān)于React封裝驗證碼輸入控件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Node的React圖片上傳組件實現(xiàn)實例代碼
本篇文章主要介紹了基于Node的React圖片上傳組件實現(xiàn)實例代碼,非常具有實用價值,需要的朋友可以參考下2017-05-05
react-router browserHistory刷新頁面404問題解決方法
本篇文章主要介紹了react-router browserHistory刷新頁面404問題解決方法,非常具有實用價值,需要的朋友可以參考下2017-12-12
React useMemo與useCallabck有什么區(qū)別
useCallback和useMemo是一樣的東西,只是入?yún)⒂兴煌?,useCallback緩存的是回調(diào)函數(shù),如果依賴項沒有更新,就會使用緩存的回調(diào)函數(shù);useMemo緩存的是回調(diào)函數(shù)的return,如果依賴項沒有更新,就會使用緩存的return2022-12-12
React?Fiber構(gòu)建completeWork源碼解析
這篇文章主要為大家介紹了React?Fiber構(gòu)建completeWork源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
webpack4 + react 搭建多頁面應(yīng)用示例
這篇文章主要介紹了webpack4 + react 搭建多頁面應(yīng)用示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
簡易的redux?createStore手寫實現(xiàn)示例
這篇文章主要介紹了簡易的redux?createStore手寫實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10

