React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React封装验证码输入控件

基于React封装一个验证码输入控件

作者:墨渊君

邮箱、手机验证码输入是许多在线服务和网站常见的安全验证方式之一,本文主要来和大家讨论一下如何使用React封装一个验证码输入控件,感兴趣的可以了解下

引言

邮箱、手机验证码输入是许多在线服务和网站常见的安全验证方式之一。这种方式通常用于确保用户在进行敏感操作(例如注册、修改密码、重置密码等)时的身份验证。

最近在做项目刚好有验证码相关的需求, 本着不重复造轮子的原则, 一顿 Google 试图找到一个现成的组件, 奈何找了一圈都没找到满意的, 要么就是交互感觉不太合理、要么就是基本停止维护了的!!

最后没办法就自己造一个了, 这里主要参考了 react-auth-code-input, 而本文则是整个思路开发流程的记录!!

DEMO 演示可查阅: blog/auth-codes

本文完整源码可查阅: coding/blog/AuthCodes

一、需求描述

开始前我们先梳理下一般验证码输入控件的常规需求有哪些:

二、布局

在开始前我们先来完成基本的布局, 如下代码所示:

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 事件的处理函数:

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 来监听键盘按下事件, 从而判断用户是否按下 删除键, 如果按下 删除键 则按照需求逻辑进行编码即可, 具体代码如下:

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>

最后效果如下: 输入验证码后, 按下删除键, 能够连续删除验证码内容

五、粘贴处理

在大部分情况下, 我们都是直接复制验证码然后直接黏贴使用, 所以我们接下来来实现的功能就是:

具体实现代码如下:

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 的变更, 做法就很简单了:

// 修改状态 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();
}, []);

十、聚焦时选中输入框内容

下面我们希望能够在输入框聚焦情况下, 能够自动选中输入框的内容, 这样的话就可以直接输入内容, 而不是先删除再输入内容!!

实现方法很简单:

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

最后效果如下:

十二、后续

到此基本差不多了, 剩下更多的可能是组件的封装上的事情, 比如:

以上就是基于React封装一个验证码输入控件的详细内容,更多关于React封装验证码输入控件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文