:::tip
最近在着手腾讯文档的输出体验优化,在其中有一个不起眼的小需要引起了我的留神,并顺便钻研了一些事件监听机制相结合的特点,特此记录一下填坑过程。
:::
<!– more –>
模仿光标跟随
大部分的支流输入法都有这样一个个性,在输出中文时,能够通过左右方向键管制光标,挪动至输入区中任意两个字符之间的地位,用户接下来的字符输出将在光标处直接插入。
因为腾讯文档的渲染的画布是齐全自主实现的,为了在体验上与一般可编辑画布保持一致,咱们须要本人来模仿这一光标的挪动行为。
首先,咱们须要确定的是输入法中的模仿光标进行更新的机会。经试验,用户在进行中文输出时,若应用了 方向键 挪动光标,将会触发光标的挪动行为。因而,首先要解决的是应用适合的事件监听来捕捉这一行为,从而进行更新。既然是对输入框的行为进行模仿,自然而然的,咱们首先想到的是输入框触发的监听器。
浏览器输入框对输出的监听机制
在浏览器对键盘的输出标准中,将键盘输入分为了间接输出与间接输出两种。间接输出将会触发输入框的 onInput
事件 (IE9 之前不反对该事件,只能用 onKeyUp
等键盘事件作为降级抉择)。而对于间接输出,标准将事件监听分为了 onCompositionStart
, onCompositionUpdate
, onCompositionEnd
三个局部。
而间接输出的同时,两头态的写入也会导致输入框内容的变动,从而也会触发 onInput
事件。因而在间接输出中,事件的触发秩序为:onCompositionStart
, onCompositionUpdate
, onInput
, onCompositionEnd
。
须要留神的是,若输出实现时,输入框的内容没有发生变化,则 onChange
事件与 onCompositionEnd
事件都将不会被触发。
中文输入法在键入选词的过程属于间接输出状况,此时两头文本不会间接落盘在输入框内。而通过回车等按键退出中文输出选词后,中文文字将会落盘到输入框,此时属于间接输出状况。
而咱们须要关注的光标事件显然是在间接输出中获取到的。在输入法选词光标左右挪动时,因为内容不变,此时并不会触发 onInput
事件,然而会触发一次 onCompositionUpdate
事件,咱们能够通过这个事件来判断光标地位,重置画布的光标地位。但最终咱们并未应用这个事件做判断器,起因在上面会讲到。
判断以后光标的地位
解决了了光标的重置机会,接下来就该解决光标的地位断定了。因为 DOM 规范中并没有间接获取光标地位的办法,因而这一块也须要咱们自主实现。我的思路是,通过选取光标到输出起始地位的字符串,判断选中的字符串长度,即可晓得光标以后地位绝对于起始地位的偏移量,从而确定光标地位。
对于一般的 input 输入框来说起始比较简单,输入框提供了 inputElement.selectionStart
属性作为以后光标地位间隔输出起始点的偏移量,咱们间接应用就能够了。然而对于 contentEditable=true
的 div 节点来说是没有这一属性的,咱们得另想办法。
依据之前写 E2E 测试得来的灵感,咱们能够模仿创立一个从以后光标地位到输出起始地位的选区,通过判断该选区的字符串长度即光标所在位置的偏移量。通过 window.getSelection()
办法可能失去 Selection 对象,这是一个示意以后文本选区的对象,因为咱们正处在输出状态中,因而该选区地位就在以后的输入框中,从而能获取到下面所需的偏移量。
const selection = window.getSelection();
// 确定输入框在输出态,存在选区
if (selection.rangeCount > 0) {const range = selection.getRangeAt(0);
return range.endOffset;
}
获取完光标地位,还须要在咱们的画布上从新设置回去。设置的思路其实是相似的,通过应用 document.createRange
办法新建一个选区范畴,其起始地位设置为须要挪动的指标地位,而后移除选区,即可使光标落在指标地位了。
性能优化
之前说到在光标挪动时确实会触发一次onCompositionUpdate
事件。然而,onCompositionUpdate
事件是一个高频的操作,每一次间接输出时都会触发,这会导致光标一直地重置地位,带来不必要的性能损失。
并且,onCompositionUpdate
事件的入参只有更新的两头字符串值,只能用来判断输出两头字符串是否发生变化。挪动光标行为自身并不会导致字符串产生扭转,但反过来,使字符串不产生扭转的操作肯定是挪动光标操作这一说法并不成立。因而,只管挪动光标会触发该事件,但咱们依然没有无效的伎俩去判断是输入法中的光标挪动导致的事件触发。
那么,之前用很大篇幅讲过光标变动的实质实际上是选区变动,那么,输入法触发的光标挪动会不会给输入框收回选区变更告诉呢?很可怜,目前绝大多数的输入法都是不反对的。并且因为光标挪动被视为输入法外部的行为,因而在输入框中光标所进行的挪动,不会有事件被动抛出。因而,输入框中的选区变更事件 onSelectionChange
事件也无奈被触发。
既然输入框中的事件监听无奈精确判断光标的挪动,咱们只能退而求其次,从更低层次的逻辑,通过监听键盘的按键输出来尝试还原这一行为了。优化思路是这样的,触发光标跟随的机会规定为:用户输出时,若应用了 左方向键 挪动光标,将会开启光标跟随的能力,随着输出不断更新的光标地位,直到光标再次被挪动到开端地位完结。因为中文输出时按下 左方向键 的行为是一个低频操作,这样一来,大部分的输出操作都不须要执行判断并重置光标,进步一般输出下的性能体现。
附上最终的判断逻辑吧:
那么,如何获取并判断用户输出时的按键信息呢?当然是应用更第一层级的事件接口 KeyboardEvent 了。
键盘输入事件对中文输入法的反对
KeyboardEvent 在低层级下提醒用户与一个键盘按键的交互是什么,不波及这个交互的上下文含意。一般来说当你须要解决文本输出的时候,该当应用上节所说的输入框监听事件代替。例如当用户应用其余形式输出文本时,如平板电脑的手写零碎等,键盘事件可能不会触发。
KeyboardEvent 对象形容了用户与键盘的交互。每个事件都形容了用户与一个按键(或一个按键和润饰键的组合)的单个交互;事件类型 keydown,keypress 与 keyup 用于辨认不同的键盘流动类型。
键盘输入事件的设计思路与间接输出的钩子相似,浏览器中对于键盘输入同样分为 onKeyDown
, onKeyPress
, onKeyUp
三个阶段的事件触发,别离对应按键不同的行为触发机会。(注:onKeyPress
事件高度依赖设施反对,所以尽量不要应用该钩子)
这三个事件都传入了 KeyboardEvent 入参,帮忙咱们理解以后执行该事件时触发的按键信息。MDN 上该入参具备如下属性反对:
在文档标准中,咱们能够发现许多对问题的解决非常有用的新属性,例如 event.isComposing
属性用于判断以后是否会触发 onCompositionUpdate
事件,event.code
用于判断与键盘布局与输出状态无关的以后按键输出,获取中文输出中的按键轻而易举。咱们能够利用这两个状态帮忙咱们实现按键监听与事件触发。
兜底计划反对
之前说过,KeyboardEvent 是一个非常依赖软硬件反对的事件,不仅须要浏览器的能力反对,与输入法甚至键盘类型都有关系。经试验后发现,这些新属性在许多浏览器与输入法的组合中都无奈通过 onKeyDown
正确获取,在 Windows 下局部中文输入法甚至都无奈反对 event.key
属性。为了达到最大的兼容性,在兜底的办法下,仅能用 event.keyCode
这种曾经被 deprecated 的办法来勉强代替应用了。
兜底计划的应用问题就此解决了吗?并没有。中文拼音的输出两头字符是零碎无奈辨认的。在 Windows 桌面应用程序对键盘输入标准中,咱们发现 Windows 将所有未辨认的设施输出都设置为 VK_PROCESSKEY 229
,浏览器的 event.keyCode
复用了这一标准,因而在中文输出过程中,无论按下什么按键,返回的 event.keyCode
永远是 229。
网上对于该问题的解决方案都是倡议应用 onKeyUp
代替 onKeyDown
。但首先,这不满足对于一个要求实时体现输出的光标挪动操作要求。第二,应用 onKeyUp
会有更多的问题,在 Windows 下进行中文输出时,因为不同的输入法回调 onKeyUp
的实现不同,该事件可能会被触发一次或两次,要么全为 229,要么一次为 229,另一次为正确的 key(对,说的就是你,搜狗)。为了防止咱们去一直去填形形色色的第三方输入法实现的坑,兜底计划采纳了当检测到输出了未辨认的按键时,也启用光标跟随能力。
结语
一套操作下来,这套中文输入法下光标跟随的性能算是完满实现了。回顾一下咱们解决这个问题所趟过的坑,实际上也反映着浏览器 JS DOM 规范在一直进化,一直补足历史遗留的坑点。当然,它还远远称不上完满,依然存在大量的能力缺失,如咱们在这个问题中遇到的判断光标偏移量的解决方案,实质上还是一种 hack。而扩大 JS 的能力边界,使其变得更弱小,更好用,这正是咱们作为前端开发人员须要致力的方向。