基于React封装一个验证码输入控件
作者:墨渊君
引言
邮箱、手机验证码输入是许多在线服务和网站常见的安全验证方式之一。这种方式通常用于确保用户在进行敏感操作(例如注册、修改密码、重置密码等)时的身份验证。
最近在做项目刚好有验证码相关的需求, 本着不重复造轮子的原则, 一顿 Google
试图找到一个现成的组件, 奈何找了一圈都没找到满意的, 要么就是交互感觉不太合理、要么就是基本停止维护了的!!
最后没办法就自己造一个了, 这里主要参考了 react-auth-code-input, 而本文则是整个思路开发流程的记录!!
DEMO
演示可查阅: blog/auth-codes
本文完整源码可查阅: coding/blog/AuthCodes
一、需求描述
开始前我们先梳理下一般验证码输入控件的常规需求有哪些:
- 假设我们验证码有
6
位, 则我们需要有6
个输入框, 每个输入框只允许输入一位数字(这里假设验证码都是数字组成) - 在输入验证码过程中, 可连续进行输入、删除等操作
- 支持黏贴复制的内容
- ...
二、布局
在开始前我们先来完成基本的布局, 如下代码所示:
- 声明状态
codes
用于存储每个验证码, 也就是每个输入框的值, 这里我将codes
设置为一个数组, 方便后面修改每个位置的验证码 - 假设我们验证码长度为
6
位, 所以这里我为codes
默认值了一个长度为6
的数组, 数组每个初始值为空字符串 - 然后我们通过
codes.map
渲染出所有输入框 - 最后我们还声明了
inputsRef
来存储所有输入框的DOM
节点, 我们后面需要通过它来调用原生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> ); };
这里我们对输入框设置了一些基本的样式
.input { width: 40px; margin: 10px; font-size: 18px; line-height: 40px; text-align: center; border-radius: 4px; border: 1px solid #d9d9d9; }
到此页面的基本效果如下:
三、动态绑定(处理 onChange 事件)
上文只是完成了基本的布局, 并且输入框 value
和状态 codes
内的值绑定在了一起, 这里输入框输入值会发现并没有生效, 那是因为状态 codes
没有被修改!
下面我们为输入框设置 onChange
事件, 在输入框输入值时动态的修改状态 codes
中对应位置的值!!
下面是 onChange
事件的处理函数:
- 两个参数,
index
和event
, 正如命名所示,index
对应输入框索位置,event
则是输入对应的change
事件对象, 通过它来获取输入值 - 特别说明, 本文验证码都是数字, 所以在函数内部还需要针对输入内容进行校验, 只允许输入数字
0~9
- 函数内还有一个特殊处理逻辑, 就是当我们输入有效值后, 需要将鼠标光标聚焦到下一个输入框, 如此用户就可以连续进行输入了, 至于实现方法很简单, 这里直接调用
inputsRef
中对应输入框DOM
节点的focus
方法即可 - 最后我们调用,
setCodes
修改状态codes
, 这样输入框的值才能动态的修改
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>
最后效果如下: 输入验证码, 光标自动跳转到下一个输入框
四、删除处理
上文我们完成验证码的输入, 但是在输入过程中, 难免会输入错误的数字, 所以就需要实现删除验证码的能力, 需求如下:
- 当我们按下
删除键
- 如果当前输入框有值, 则删除当前输入框中的内容
- 如果当前输入框没有值, 则删除上一个输入框内容, 并且聚焦到上一个输入框
需求其实已经很明确了, 我只需要通过 onKeyDown
来监听键盘按下事件, 从而判断用户是否按下 删除键
, 如果按下 删除键
则按照需求逻辑进行编码即可, 具体代码如下:
- 函数接收两个参数
index
和event
,index
表示当前光标所在的输入框索引位置,event
则是事件对象 - 通过
event.key
的值来确定是否按下删除键
(Backspace
), 如果不是, 则不进行任何处理 - 剩下就按需求来, 如果当前输入框有值则清除当前输入框内容
- 如果当前输入框没值, 则清除上一个输入框内容, 并且将光标移到上一个输入框中, 这里还需要考虑下边界情况, 如果当前输入框已经是第一个了, 就无需进行任何处理
const handleDelete = useCallback((index, event) => { const { key } = event; // 是否按下删除键, 否提前结束 if (key !== 'Backspace') { return; } // 1. 如果当前输入框有值, 则删除当前输入框内容 if (codes[index]) { setCodes((pre) => { const newData = [...pre]; newData[index] = ''; return newData; }); } else if (index > 0) { // 2. 如果当前输入框没有值(考虑下边界的情况 index === 0): 则删除上一个输入框内容, 并且光标聚焦到上一个输入框 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>
最后效果如下: 输入验证码后, 按下删除键, 能够连续删除验证码内容
五、粘贴处理
在大部分情况下, 我们都是直接复制验证码然后直接黏贴使用, 所以我们接下来来实现的功能就是:
- 允许在任意输入框黏贴数据
- 自动将剪切板的数字回填到输入框中
- 这里不做过多的处理, 不管光标在哪个位置, 都从第一个输入框开始填充数字
- 注意的是, 这里光标还需要自动聚焦到最后一个输入框内容为空的位置
具体实现代码如下:
- 通过
event.clipboardData.getData
获取到剪切板内容 - 过滤掉剪切板中非数值部分内容
- 生成新状态
codes
: 先创建了一长度为6
的数组, 并使用剪切板的数字就行填充, 不够的用空字符进行填充, 最后使用setCodes
来修改状态值 - 光标位置修改, 根据剪切板数字长度来进行计算
const handlePaste = useCallback((event) => { const pastedValue = event.clipboardData.getData('Text'); // 读取剪切板数据 const pastNum = pastedValue.replace(/[^0-9]/g, ''); // 去除数据中非数字部分, 只保留数字 // 重新生成 codes: 6 位, 每一位取剪切板对应位置的数字, 没有则置空 const newData = Array.from( { length: 6 }, (_, index) => pastNum.charAt(index) || '', ); setCodes(newData); // 修改状态 codes // 光标要聚焦的输入框的索引, 这里取 pastNum.length 和 5 的最小值即可, 当索引为 5 就表示最后一个输入框了 const focusIndex = Math.min(pastNum.length, 5); inputsRef.current[focusIndex]?.focus(); }, []);
最后为每个输入框绑定 onPaste
(黏贴) 事件
<input ... onPaste={handlePaste} />
最后效果如下: 光标聚焦在任意输入框, 进行黏贴后, 即可自动用剪切板内的数字来填充输入框
六、第一阶段完成
到此整体功能已经差不多了, 下面是目前为止完整的代码(删除了 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; // 是否按下删除键, 否提前结束 if (key !== 'Backspace') { return; } // 1. 如果当前输入框有值, 则删除当前输入框内容 if (codes[index]) { setCodes((pre) => { const newData = [...pre]; newData[index] = ''; return newData; }); } else if (index > 0) { // 2. 如果当前输入框没有值(考虑下边界的情况 index === 0): 则删除上一个输入框内容, 并且光标聚焦到上一个输入框 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'); // 读取剪切板数据 const pastNum = pastedValue.replace(/[^0-9]/g, ''); // 去除数据中非数字部分, 只保留数字 // 重新生成 codes: 6 位, 每一位取剪切板对应位置的数字, 没有则置空 const newData = Array.from( { length: 6 }, (_, index) => pastNum.charAt(index) || '', ); setCodes(newData); // 修改状态 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> ); };
基本功能有了, 下面我们对组件进行简单的封装、优化....
七、暴露 onChange 事件
这里我们希望父组件可以通过 onValueChange
来监听到内部状态 codes
的变更, 做法就很简单了:
- 抽离一个通过方法
resetCodes
, 修改状态的地方全部使用resetCodes
方法 resetCodes
方法内部则是调用setCodes
方法修改codes
同时调用父组件传进来的onValueChange
方法resetCodes
支持传一个数组进来, 也可以是一个index
一个value
; 这么做的原因主要是为了支持不同场景下修改状态codes
的需求
// 修改状态 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]);
最后还需要将代码里调用 setCodes
的地方改为 resetCodes
, 这里就不做演示了; 修改完成之后, 我们就可以通过 onValueChange
监听到组件内部 codes
的变更了
<AuthCode onValueChange={(codes) => console.log(codes)} />
最后效果如下:
八、暴露 onComplete 事件
这里我们还希望在输入完所有验证码后, 能够被组件外部监听到, 这样就可以直接拿到完整的验证码向后端服务发起校验....
其实有了上面的基础, 我们可以直接在 resetCodes
中进行处理: 在修改状态 codes
前判断下所有验证码是否都已经输入, 如果已全部输入则调用父组件的 onComplete
事件
// 修改状态 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
监听到
<AuthCode onComplete={(codes) => console.log(codes)} />
最后效果如下:
九、自动聚焦
这个需求就很简单咯, 就是希望组件在初始化时可以将鼠标光标自动聚焦到第一个输入框, 这样用户就可以直接进行输入, 完成验证码的校验!!!
实现方法就更简单, 直接在 useEffect
中调用第一个输入框的 DOM
节点的原生 focus
方法即可
useEffect(() => { inputsRef.current[0].focus(); }, []);
十、聚焦时选中输入框内容
下面我们希望能够在输入框聚焦情况下, 能够自动选中输入框的内容, 这样的话就可以直接输入内容, 而不是先删除再输入内容!!
实现方法很简单:
- 通过
onFocus
事件来实现, 监听Focus(获取焦点)
事件 - 然后在事件处理函数内调用事件
select
方法来选中输入框的内容
const handleOnFocus = useCallback((e) => { e.target.select(); }, []);
最后效果如下:
十一、暴露外面接口
最后我们希望父组件可以通过 ref
来获取到一些组件内部预设好的方法, 比如自动获取焦点、清空所有输入框内容等等
如下代码使用 forwardRef
配合 useImperativeHandle
完成 ref
的绑定
export default forwardRef((props, ref) => { // ... useImperativeHandle(ref, () => ({ // 获取焦点 focus: (index = 0) => { if (inputsRef.current) { inputsRef.current[index].focus(); } }, // 清空内容 clear: () => { resetCodes(codes.map(() => '')); }, })); // ... }
调用方法如下所示:
export default () => { const ref = useRef(); return ( <> <Com ref={ref} /> <Button onClick={() => ref.current?.clear()}> 清空 </Button> </> ); };
最后效果如下:
十二、后续
到此基本差不多了, 剩下更多的可能是组件的封装上的事情, 比如:
- 允许设置默认值
- 支持双向绑定
- 支持设置验证码长度
- 支持设置验证码规则(纯数字、纯字母、字母数字混合)
- 支持设置
input
参数(比如placeholder
等等) - ...
以上就是基于React封装一个验证码输入控件的详细内容,更多关于React封装验证码输入控件的资料请关注脚本之家其它相关文章!