:::tip
最近在著手騰訊檔案的輸入體驗優化,在其中有一個不起眼的小需求引起了我的注意,并順便研究了一些事件監聽機制相結合的特點,特此記錄一下填坑程序,
:::
模擬游標跟隨
大部分的主流輸入法都有這樣一個特性,在輸入中文時,可以通過左右方向鍵控制游標,移動至輸入區中任意兩個字符之間的位置,用戶接下來的字符輸入將在游標處直接插入,
由于騰訊檔案的渲染的畫布是完全自主實作的,為了在體驗上與普通可編輯畫布保持一致,我們需要自己來模擬這一游標的移動行為,
首先,我們需要確定的是輸入法中的模擬游標進行更新的時機,經試驗,用戶在進行中文輸入時,若使用了方向鍵移動游標,將會觸發游標的移動行為,因此,首先要解決的是使用合適的事件監聽來捕獲這一行為,從而進行更新,既然是對輸入框的行為進行模擬,自然而然的,我們首先想到的是輸入框觸發的監聽器,
瀏覽器輸入框對輸入的監聽機制
在瀏覽器對鍵盤的輸入規范中,將鍵盤輸入分為了直接輸入與間接輸入兩種,直接輸入將會觸發輸入框的 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 的能力邊界,使其變得更強大,更好用,這正是我們作為前端開發人員需要努力的方向,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/514197.html
標籤:其他
