记录--被中文输入法坑死了
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
PM:在PC端做一个@功能吧,就是那种...。
我:你不用解释🤔我知道那个功能,监听keydown
事件,然后e.keycode === 50
,那可太简单了。
那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声)
坑1:KeyBoardEvent.keycode
废弃的属性你就坚持用吧,一用一个不吱声。以后线上跑得好好的代码突然报错了,你都不知道bug在哪儿。
现在的web标准里,要确定一个键盘事件是靠e.key
和e.code
。code
代表触发事件的物理按键,比如2的位置code='Digit2'
。key
返回用户按下的物理按键的值,它还与 shiftKey 等调节性按键的状态、键盘的区域和布局有关。
所以对于@
来说,直接判断e.key === "@"
来做后续的操作就行了。
addEventListner('keydown', (e) => { if (e.key === "@") { e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理 // 插入有@字符的,可监听输入的元素 // 唤起小窗.... } });
仔细看上面的这几行代码和注释,要开始考(坑)了。
坑2:输入法的坑
起因
在我美滋滋地以为避过了坑1就没事了的时候,一个夜晚我的测试同学告诉我,在测试环境突然就体验不到这个功能了,无论输入多少个@都不行,白天还好好的🤯。
好一个「白天还好好的」。
我自己测试的时候又百分百能体验到🤔,所以最开始我还在怀疑他没有配上测试系统......
于是,让测试同学的windows电脑连到我的开发环境debug一看:
好家伙,真是好家伙😅他的电脑的e.key === "Process"
????!!!
什么意思呢,就是正常我们理想中的@字符产生是shift+2按键的组合,监听keydown
之后我们会按顺序收到两个回调:
e.key === "Shift"
,e.code === "ShiftLeft"
或者shiftRighte.key === "@"
,e.code === "Digit2"
但是实际在测试同学的电脑里,1是一样的,但是2变了,2变成了e.key === "Process"
。
虽然键盘事件有变化,但是在前端页面上的@字符是没有任何变化的。难怪他说他会突然失效了。我问他做了什么怎么会突然变了,他想了想说晚上从系统输入法换成了微信输入法.....
上网检索(chatGPT)了一番,明白了一个新的知识点:
输入法的全称叫Input Method Editor输入法编辑器(IME)。本质上它也是个编辑器。为了能输入各类字符(比如汉字和阿拉伯字等),IME会先处理用户的英文字母输入然后通过系统的底层调用传递给浏览器,浏览器再显示到用户的界面。这里的Process
很大概率就是当时输入法给出的某个值表示那个时刻它还在处理中。
解决办法
既然KeyBoardEvent
靠不住,那我们换一种监听方式。
我找到了一个非常适用于输入法的监听事件叫做CompositionEvent
,它表示用户间接输入文本(如使用输入法)时发生的事件。此接口的常用事件有compositionstart
, compositionupdate
和 compositionend
。它们三个事件分别对应的动作,通俗一点说就是你用输入法开始打字、正在打字和结束打字。
于是乎,我监听compositionend
不就行了!在输入法end的时候我再去看你end的字符是不是@不就行了!
// addEventListner('keydown', (e) => { addEventListner('compositionend', (e) => { // if (e.key === "@") { if (e.data === "@") { e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理 // 插入有@字符并且可监听输入的元素 // 唤起小窗.... } });
对于输入法来说,按键的up和down的key
值就算不尽人意也没什么损失,毕竟用户毫无感知。但是,compositionend
永远是不会错的,如果compositionend
的e.data
都不是@字符了,那么在用户的编辑器界面上的显示肯定也会跟着出错。
所以监听这个肯定就是万无一失的方法了,哈哈哈我真是个“天才”(蠢材)。 修改之后让测试同学尝试之后果然就可以了。
坑3:输入法继续坑
起因
时间过去了没一会,本天才就收到了另一个测试同学反馈的问题说为什么输入了一个@字符之后,会出现两个@在界面上?
我第一反应就是难道没有执行到e.preventDefalut()
?既然后续功能能正常使用,没执行到也不应该啊🤔。然后在我电脑一通尝试,发现safari浏览器在输入法为中文的情况下也会触发这个问题。
于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开二度):
执行到了,也没有报错什么的,但是@字符并没有被prevent掉🤯。
再加上我自己传入的@,所以界面上就出现了两个@字符。啊这这这,这很难评......
我是左思右想,百思不得其解,于是只能:
上面大概的意思就是compositionend
事件里使用 e.preventDefault()
在技术上可行的,但它可能不会产生你期望的效果。可能是因为 compositionend
事件标志着一次输入构成(composition)会话的结束,而在这个点上阻止默认行为可能没有意义,因为输入的流程已经完成了。
更推荐用keydown
,compositionstart
和input
来处理这种情况。
那keydown
是不可能keydown
了,已经被坑了。compositionstart
也不行,因为刚开始输入那会才按下了shift键,@字符还没出来呢。那就只能input
了。
解决办法
最开始我没有选择input
就是因为它不能使用e.preventDefault()
。我必须要对输入的字符串进行单独处理,去掉@,当时觉得很麻烦就没有选择这个方法。
额....好好好,行行行,现在还是必须得处理一下了。
// addEventListner('keydown', (e) => { // addEventListner('compositionend', (e) => { addEventListner('input', (e) => { // if (e.key === "@") { if (e.data === "@") { ?怎么去处理字符呢 // 这里去掉@字符是为了后续插入和监听方便处理 // 插入有@字符并且可监听输入的元素 // 唤起小窗.... } });
对于这个处理字符的方法,也是一个新知识点了。起初我还想的是去处理编辑器里的content,然后再给它插入回去,这样子复杂度很高并且出错的概率极大。
这里的解决办法主要是使用CharacterData接口。CharacterData 接口是操作那些包含字符数据的节点的核心,特别是在需要动态地更改文本内容时。
例如,在一个文本节点上使用 deleteData()
方法可以从文本中移除一部分内容,而不必完全替换或重写整个节点。
const selection = window.getSelection(); const { anchorNode, anchorOffset } = selection; if (anchorNode.substringData(anchorOffset - 1, 1) === '@') { selection.anchorNode.deleteData(anchorOffset - 1, 1); }
写完这个之后,我用自己的safari浏览器测试发现果然没有问题了。
哈哈哈我真是个“天才”(蠢材)。
坑4:输入法深坑🕳️
我自信满满地让测试同学再重试一下😎,然后测试同学说:和之前一样啊,还是有两个@字符。
我:啊?啊啊??啊啊啊???
于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开三度):
发现测试同学电脑上的anchorOffset
和正常的情况下是不一样的,会小一位,所以导致anchorOffset - 1
拿到的前一个字符并不等于@,所以后续也没有把它处理掉🤯。
我是左思右想,百思不得其解,stack overflow上也没有相关的问题。不过,结合IME的概念,肯定还是输入法的问题。
结合之前keydown的e.key==="Processing"
,可能在input
触发时输入法的编辑器其实还是没有完成工作(composition),导致在那个时候Selection
的anchorOffset
不一致。其实浏览器的Selection
肯定不会错,那anchorOffset
看起来像是错了,我觉得应该是输入法在转换的过程对我们的前端页面做了一些用户看不到的东西,而anchorOffset
把它显化出来罢了。
解决办法
于是乎,我尝试性的对处理字符串的那串代码进行延时,目的是为了等待输入法彻底工作完毕。
// addEventListner('keydown', (e) => { // addEventListner('compositionend', (e) => { addEventListner('input', (e) => { // if (e.key === "@") { if (e.data === "@") { setTimeout(() => { const selection = window.getSelection(); const { anchorNode, anchorOffset } = selection; if (anchorNode.substringData(anchorOffset - 1, 1) === '@') { selection.anchorNode.deleteData(anchorOffset - 1, 1); } // 这里去掉@字符是为了后续插入和监听方便处理 }); // 插入有@字符并且可监听输入的元素 // 唤起小窗.... } });
然后,问题真的就彻底解决了。
这个功能做起来可太简单了......😅