扫码与中文输入法

一、什么是扫码输入

扫码输入就是用扫码枪或者其他设备扫描图形码(条形码或其他码)后将其内容识别为文本输入的操作。
扫码能减少降低成本,降低输入出错率,提高输入效率。
比较常见场景有的快递取货、入库、出库等。

1.扫码枪扫码的时候做了什么

扫码枪可以看做是一种特殊的键盘,识别图形码的内容之后,将内容以键盘输入的形式输出按键码。
扫码识别内容成功之后会触发键盘事件,实际就是模拟键盘按键得过程,和键盘一样,会触发“onkeydown/onkeyup”事件,当识别的文本全部触发完成之后会自动调用“回车事件”。
扫码
比如将“123456” 几个数字生成一个条形码,一次扫码就相对于“快速输入 123456”,然后快速输入“回车”键。

2.如何区分扫码枪和键盘

既然扫描相对于快捷键盘输入,那么我们能区分扫码枪和键盘吗?
通过正常途径来判断是不行的,毕竟都是“键盘输入”事件,扫码枪也没有做区分。

不过也有方法区分,那就是利用键盘事件的响应间隔。
一般普通人在键盘上连续输入多个字符的间隔都在 30ms 以上,而扫码枪触发的输入,间隔一般都在 10ms 以内
因此,我们可通过多个按键之间的间隔时间进行 Hack 判断。

二、扫码中文输入法的坑

正常来说,对一个值进行扫码输入是没问题的,但是如果扫码枪是连接在电脑上操作且当前电脑切换未中文输入法的时候那就麻烦了。
前面说了扫码就相当于“键盘字符输入+回车”。

可以实际操作一下,将当前电脑输入法切换至中文,然后模拟当条形码的值为“qwe1”的时候,此时扫码(输入)会发生什么?
扫码
此时按一个“1”又会得到什么?

答案是得到一段中文,显然这不是我们期望的。

三、如何解决中文扫码

最最最简单推荐的方法就是在页面上做一个提示,告诉用户,“这里的输入需切换到英文输入法,不然可能结果会不符合预期”。

如果能说服产品经理这样干,那就不用往下看了;如果说服不了那就继续看下面的吧。

核心思路就两个

方法 1)使用 <input type="password" /> 代替普通输入框,然后使其不可见,再写一个可见的元素(可以是 input)显示其内容。

方法 2)监听所有的输入 keycode 值,保存扫码枪扫码字符的“过程”值,忽略其输入框中的“最终”值,然后用保存的过程值作为最终的扫码结果。

四、Password 方案

先说结论,这个行不通。

为什么行不通?
因为坑特别多,下面会详细介绍,供参考。

此方案用两个 input,一个 input[type=’password’],一个 text[type=’input’]。
二者用 css 相对定位进行重叠,text[type=’input’] 可见在下,input[type=’password’] 不可见在上。
通过监听 input[type=’password’] 的输入,将值同步给 text[type=’input’] 显示内容。

1
2
3
4
5
6
7
8
<input
type="password"
value={{value}}
onInput={(e) => {
setValue(`${e?.target?.value}`)
}}
/>
<input type="text" value="{{value}}" />

针对如上 html,我们可以将 input[type=’password’] 整个透明度设置为 0(也可以针对其背景、边框、文字分别设置透明度为 0)。

接下来问题就开始了
首先,样式设置好之后就会发现个明显的问题,“输入的时候光标不见了”。
原因就是透明之后光标也被透明了,而text[type=’input’]又没有聚焦。

有两个办法解决此问题:
1)将 input[type=’password’] 的背景颜色设置透明度为 0,将光标单独设置颜色。

1
2
background: transparent;
caret-color: #181818;

这个能解决光标不可见的问题,但是解决之后又发现个新的问题。
由于叠加 input[type=text]和 input[type=’password’]的文字所占宽度不一致,如”1”和”9”,”1”和”*“,”a”和”A”所占宽度都不一样。
所以,属于 input[type=’password’] 的光标位置和实际看到的内容末尾的位置是对不上的,如“A87”和“***”所占宽度不一致导致光标位置错位了。

解决办法:

1
2
1)设置 leterspace。
2)将 input[type='text']替换成 ul>li,然后对每个元素设置固定宽度。

通过上面两个配置,然后细调一下宽度,就能做到宽度一致,光标自然也显示正常了。

但是此法却引入新的问题
这会导致数字与其他字符之间稀拉拉不紧凑,比如数字 1 和 8 之间,8 与 9 之间的间距会不一样;* * *之间的宽度会变宽。
扫码

再尝试
尝试对 type[password]动态计算宽度,其宽度和对 ul 动态计算出来的宽度保持一致

1
setPasswordInputWidth(!value ? 1 : strUlRef?.current.offsetWidth);

但是仍然对不齐,因为虽然元素的宽度一致了,文字内容的宽度却还是不一致。
扫码

于是,不得不对 input[type=password] 设置 text-align:right
这样设置之后光标与末尾的元素的位置算是对齐了。

然而还是有问题,因为虽然末尾边文字虽然对齐了,最前面开头部分又对不齐了。
这就导致选中元素的时候选中的“阴影”对不上。

没办法,继续尝试第二个方法
2)通过 span + css3动画手动模拟一个光标,动态设置其距离最左边的宽度为可见内容的宽度。

1
cursorRef.current.style.left = textRef.current.offsetWidth;

经过验证,此方法可行,能解决光标对不齐的问题,只是选中“阴影”的问题依然存在,没有好的办法。

除此之外,解决光标问题之后,还有新的问题。

input[type=password]会存在自动回填提示。
扫码

这个是浏览器的特性,虽然根据国际标准,理论上是可以通过设置autocomplete等属性来设置其不自动回填的。
但是做过的都知道,这个属性没有浏览器严格执行,在 Chrome 上尤其不可行。

因此,虽然解决办法有“很多“,有说设置隐藏 password 的,有说 readonly 的,试过,全都不行。

不过,虽然常规方式不行,但是本人经过测试发现一个规律。
只有 input[type=password] 清空的时候才会提示如上图的密码账号推荐值,当输入框里面有值的时候却不会显示。

于是,灵机一动,就在 input[type=password]的前面加一个或者加 N 个空格字符且令其无法清除
然后再其显示的时候动态过滤之后再显示,这个虽然有些麻烦,但是也算是解决问题了。

原以为结束了,但是在测试的时候又发现了新的问题:input[type=password]无法进行 ctl+c、ctl+x。
这个也是浏览器的特性,好像也没有好办法能直接解决。

不过,仔细想想这些也可以手动监听键盘事件来解决。
只是还得考虑右键复制,选择字符多少,是全选还是选几个、组合键会不会影响扫码输入等等。
由于这个实现还有些复杂本人就没有实践(已经决定放弃此方案了)。

除此之外,由于是 input[type=password] 输入框,当聚焦的时候地址栏会多一个钥匙图标,不过这个问题不大,也在可以忍受的范围内吧。

。。。

至此,已经写了很长很长的代码了,不知道是否还有新的问题。
或许有,或许没有了吧。
但是,先不论还有没有新的问题,就说光解决上面已知的这些问题都让我不想继续下去了,不是偷懒,而是太过于复杂的方案绝对不是一个好的方案。

所以,最终我还是决定放弃此方案了。

五、最终扫码方案

既然 Password 方案行不通,那就只能试用第二种方式了,通过全局监听 onkeydown 事件来处理。
onkeydown 依然比较复杂,也有一些小坑,但是相比于前者真的是好太多太多了。

此方案主要思路

1
2
3
监听页面上所有的输入 keycode 值,判断是否为扫码输入。
如果为扫码输入则保存扫码枪扫码字符的“过程”值,忽略其输入框中的“最终”值。
最后当回车键触发的时候,用保存的过程值作为最终的扫码结果。

核心实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let timeStamp = 0;
const keydownHandle = (e) => {
// 使用事件里面 timeStamp 判断触发时间小于 30 毫秒是否是扫码
const isScan = e.timeStamp - timeStamp < 30;
timeStamp = e.timeStamp;
const inputElement = scannerInputRef?.current;
if (!isScan) {
// 非扫码直接置空
processCodesRef.current = "";
}
if (e.keyCode === 13) {
if (isScan) {
// 直接替换为过程值
setValue(processCodesRef.current);
inputElement.value = processCodesRef.current;
}
} else if (
(e.keyCode >= 96 && e.keyCode <= 105) || // 数字键盘
(e.keyCode >= 48 && e.keyCode <= 57) || // 数字
(e.keyCode >= 65 && e.keyCode <= 90) || // 字母
e.keyCode === xxx // 其它扫码支持的字符
) {
processCodesRef.current += e.key;
}
};

经过测试和验证,上面这个方案没有明显硬伤,唯一发现的问题就是中文输入法扫码字母和数字的时候会出现一些中文输入显示的过程。
但是由于我们最终会将过程值替代输入的结果,所以最终结果还是没问题的。

同时,在网上看到别人说会丢失一些字符,这个我倒是没有遇到过。

个人觉得,扫码的时候切换为中文输入法本来就可以算是一个“异常”场景,既然最终功能没问题,中间出现一些中文输入过程也不是不可接受。

ps:也没有更好的办法了,如果扫码在站点是一个非常高频的操作,还是建议从产品层面给个提示让用户切换为英文输入。


附录、无焦点扫码优化

针对扫码输入,理论上来说是需要首先聚焦 input 输入框的,但是为了更好的用户体验,即使没有聚焦我们也可以做一些优化手段来让用户正常扫码的。

在网页里面除了输入元素有焦点事件,浏览器网页本身也有焦点事件。
就跟我们其他 pc 应用一样,刚打开应用的时候该应用都处于 focus 状态。
而当浏览器处于 focus 的时候其实是能够正常接收到“键盘事件”的(只是我们没有输入元素给用户看得见)。

因此,针对无焦点输入得分为两种情况处理,一种是整个网页页面都没有焦点,这种情况是系统层面的行为,我们没办法处理。
另一种就是浏览器网页本身是聚焦的,input 输入框未聚焦,这种情况还是可以做一些事情的。

对于 input 无焦点的解决办法就是,全局监听

1)网页监听全局的“键盘事件”
2)然后通过“间隔时间 Hack”等特性判断是否是扫码枪触发的。
3)根据这些特性判断确实是期望的输入值之后通过 JS 将其回显到对应的输入框即可。

当然,既然做到了这一层,那么根据扫描的“码”的内容来判断输入的是什么码,然后根据内容将特定的内容填写到对应的 input 框,这也能顺手解决一个页面有多个输入框自动扫码触发输入框聚焦的情况。