扫码枪:就是超市付钱时营业员手持扫码设备,还有一些扫码pos设备和扫码枪一样的原理。 如图
最近就有一个需求关于手持扫码枪的,有个列表是通过订单号搜的,手动输入太慢,效率也低,需要支持一下扫码枪输入。本文将探索如何封装一个
useScanCodeGun()的hook来监听扫码枪事件,解决这类场景。
探索
调研后会发现其实扫码枪作为外设,其实触发的就是keydown事件,只需要监听keydown即可。
扫描这个码其实等同于 键盘输入 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)
测试码
第一版
根据监听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键。
第二版
针对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);
};
}, []);
}
这里输出就是正确的,到这里就可以解决大部分场景了,但是还有问题。
问题一
这里只是监听Enter键作为结束,如果手动Enter,或者我手动按了一些键再手动Enter,也会触发onChang事件。如果手动聚焦输入框并输入1234+Enter+Enter,这里就会出问题。
问题二
当手动输入aaa在扫码,这里也是出现了问题
第三版
上一版的原因主要还是因为监听到了手动输入。主要解决的问题还是如何区分是手动输入还是扫码枪输入。
后来也是查了一下,并没有方案可以区分。这里只能自己来,手动输入和扫码枪输入的唯一区别就是输入的速度,扫码枪输入比手动快的多,以这个为切入点的第三版。
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);
};
}, []);
}