前端如何监听扫码枪扫码事件——自定义useScanCodeGun()

4,582 阅读3分钟

扫码枪:就是超市付钱时营业员手持扫码设备,还有一些扫码pos设备和扫码枪一样的原理。 如图

image.png

image.png

最近就有一个需求关于手持扫码枪的,有个列表是通过订单号搜的,手动输入太慢,效率也低,需要支持一下扫码枪输入。本文将探索如何封装一个useScanCodeGun()的hook来监听扫码枪事件,解决这类场景。

探索

调研后会发现其实扫码枪作为外设,其实触发的就是keydown事件,只需要监听keydown即可。

image.png

扫描这个码其实等同于 键盘输入 12345 + Enter

但是真实场景还会有很多问题。如果要求不高的话,可以直接手动聚焦输入框,直接扫码就能输入也可以实现,但是体验并不好,每次输入都需手动聚焦输入框,输入之后需要手动删除才能再次输入。

需求

这里我在详细的描述一下我的需求,以及我想要达到的效果。

//useScanCodeGun 的使用
function Test() {
  useScanCodeGun({
    onChange: (value) => {
      console.log('value', value);
       //TODO: 回填input,并搜索
    },
  });
  return <div>Test</div>;
}

1.不需要任何操作只需要引入useScanCodeGun()这个hook,直接扫码就可以拿到当前扫码值。(扫码就会log出扫码值)

2.每次扫码拿到的都是当前扫码的值。不会记录上一次扫码。(每次log的都是当前值)

3.并不会监听用户手动的keydown事件。(用户手动按键,不会log)

测试码

image.png

第一版

根据监听keydown事件来实现的,监听Enter作为输入结束的标志。

export function useScanCodeGun({ onChange }) {
  const inputBuffer = useRef(''); //缓冲区

  useEffect(() => {
    const handleKeyDown = (event) => {
      if (event.key === 'Enter') {
        onChange?.(inputBuffer.current);
        inputBuffer.current = ''; // 清空缓冲区
      } else {
        inputBuffer.current += event.key;
      }
    };
    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);
}

问题

扫码测试码的结果,发现大写字母会多出一个Shift键。

image.png

第二版

针对Shift键,这是使用正则做了校验

export function useScanCodeGun({ onChange }) {
  const inputBuffer = useRef(''); //缓冲区

  useEffect(() => {
    const handleKeyDown = (event) => {
      // 只允许 字母 和 数字 和 -
      const isLetterOrDigit = /^[a-zA-Z0-9\-]$/.test(event.key);

      // 扫码枪输入
      if (event.key === 'Enter') {
        onChange?.(inputBuffer.current);
        inputBuffer.current = ''; // 清空缓冲区
      } else if (isLetterOrDigit) {
        inputBuffer.current += event.key;
      }
    };
    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);
}

这里输出就是正确的,到这里就可以解决大部分场景了,但是还有问题。

image.png

问题一

这里只是监听Enter键作为结束,如果手动Enter,或者我手动按了一些键再手动Enter,也会触发onChang事件。如果手动聚焦输入框并输入1234+Enter+Enter,这里就会出问题。

image.png

问题二

当手动输入aaa在扫码,这里也是出现了问题

image.png

第三版

上一版的原因主要还是因为监听到了手动输入。主要解决的问题还是如何区分是手动输入还是扫码枪输入

后来也是查了一下,并没有方案可以区分。这里只能自己来,手动输入和扫码枪输入的唯一区别就是输入的速度,扫码枪输入比手动快的多,以这个为切入点的第三版。


export function useScanCodeGun({ onChange }) {
  const inputBuffer = useRef('');
  const lastKeyPressTime = useRef(Date.now()); // 上次按键时间
  const lastKey = useRef(''); // 上次按键
  const isFirstScanPress = useRef(true); // 是否是第一次进入扫码枪输入
  const saveFirstKey = useRef(''); // 用于保存第一次进入扫码枪输入的上一次按键
  const Reg = /^[a-zA-Z0-9\-]$/;

  useEffect(() => {
    const handleKeyDown = (event) => {
      const currentTime = Date.now();

      // 计算输入时间间隔
      const timeSinceLastKeyPress = currentTime - lastKeyPressTime.current;
      // 判断是否为扫码枪输入(以50ms内连续输入间隔来判断是否为扫码枪输入)
      const isScanPress = timeSinceLastKeyPress < 50;
      // 只允许字母和数字
      const isLetterOrDigit = Reg.test(event.key);
      if (isScanPress) {
        // 扫码枪输入
        if (isFirstScanPress.current) {
          saveFirstKey.current = lastKey.current; // 保存第一次输入的上一次按键
          isFirstScanPress.current = false;
        }
        if (event.key === 'Enter') {
          isFirstScanPress.current = true;
          const is = Reg.test(saveFirstKey.current);
          const midValue = `${is ? saveFirstKey.current : ''}${inputBuffer.current}`;
          onChange?.(midValue);
          saveFirstKey.current = ''; //
          inputBuffer.current = ''; // 清空缓冲区
        } else if (isLetterOrDigit) {
          inputBuffer.current += event.key;
        }
      } else {
        // 非扫码枪输入,不处理
      }
      // 更新上次按键时间
      lastKeyPressTime.current = currentTime;
      // 更新上次按键
      lastKey.current = event.key;
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);
}

这里以 前后两次输入间隔时间<50ms 的标志来判断是的扫码枪输入。

这里我测了一下

扫码枪的输入间隔都在5ms~20ms之间。

手动输入时间隔时一般都大于100ms,如果故意输入很快也可能会被判断扫码枪输入(故意快速输入暂时无法避免,如果有解决办法的朋友可以评论区里讲一下)

完整代码

这样就可以支持手动输入,也支持扫码枪输入,扫码输入每次都会覆盖为新扫码的。

  function Test() {
  const [value, setValue] = useState();
  useScanCodeGun({
    onChange: (value) => {
      console.log('value', value);
      setValue(value);
    },
  });

  return (
    <div>
       <input value={value} onChange={(e) => setValue(e.target.value)} type="text" />
    </div>
  );
}
  

  
export function useScanCodeGun({ onChange }) {
  const inputBuffer = useRef('');
  const lastKeyPressTime = useRef(Date.now()); // 上次按键时间
  const lastKey = useRef(''); // 上次按键
  const isFirstScanPress = useRef(true); // 是否是第一次进入扫码枪输入
  const saveFirstKey = useRef(''); // 用于保存第一次进入扫码枪输入的上一次按键
  const Reg = /^[a-zA-Z0-9\-]$/;

  useEffect(() => {
    const handleKeyDown = (event) => {
      const currentTime = Date.now();

      // 计算输入时间间隔
      const timeSinceLastKeyPress = currentTime - lastKeyPressTime.current;
      // 判断是否为扫码枪输入(以50ms内连续输入间隔来判断是否为扫码枪输入)
      const isScanPress = timeSinceLastKeyPress < 50;
      // 只允许字母和数字
      const isLetterOrDigit = Reg.test(event.key);
      if (isScanPress) {
        // 扫码枪输入
        if (isFirstScanPress.current) {
          saveFirstKey.current = lastKey.current; // 保存第一次输入的上一次按键
          isFirstScanPress.current = false;
        }
        // 扫码枪输入
        if (event.key === 'Enter') {
          isFirstScanPress.current = true;
          const is = Reg.test(saveFirstKey.current);
          const midValue = `${is ? saveFirstKey.current : ''}${inputBuffer.current}`;
          onChange?.(midValue);
          saveFirstKey.current = ''; //
          inputBuffer.current = ''; // 清空缓冲区
        } else if (isLetterOrDigit) {
          inputBuffer.current += event.key;
        }
      } else {
        // 非扫码枪输入,不处理
      }
      // 更新上次按键时间
      lastKeyPressTime.current = currentTime;
      // 更新上次按键
      lastKey.current = event.key;
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);
}