?(原標(biāo)題:被中文輸入法坑死了)
PM:在PC端做一個(gè)@功能吧,就是那種...。

我:你不用解釋??我知道那個(gè)功能,監(jiān)聽(tīng)keydown事件,然后e.keycode === 50,那可太簡(jiǎn)單了。
那可太簡(jiǎn)單了,可太簡(jiǎn)單了,太簡(jiǎn)單了,簡(jiǎn)單了,單了,了......(掉進(jìn)坑里的回聲)
坑1:KeyBoardEvent.keycode
廢棄的屬性你就堅(jiān)持用吧,一用一個(gè)不吱聲。以后線上跑得好好的代碼突然報(bào)錯(cuò)了,你都不知道bug在哪兒。
現(xiàn)在的web標(biāo)準(zhǔn)里,要確定一個(gè)鍵盤(pán)事件是靠e.key和e.code。code代表觸發(fā)事件的物理按鍵,比如2的位置code='Digit2'。key返回用戶按下的物理按鍵的值,它還與 shiftKey 等調(diào)節(jié)性按鍵的狀態(tài)、鍵盤(pán)的區(qū)域和布局有關(guān)。
所以對(duì)于@來(lái)說(shuō),直接判斷e.key === "@"來(lái)做后續(xù)的操作就行了。
addEventListner('keydown', (e) => {
if (e.key === "@") {
e.preventDefalut();
}
});
仔細(xì)看上面的這幾行代碼和注釋?zhuān)_(kāi)始考(坑)了。
坑2:輸入法的坑
起因
在我美滋滋地以為避過(guò)了坑1就沒(méi)事了的時(shí)候,一個(gè)夜晚我的測(cè)試同學(xué)告訴我,在測(cè)試環(huán)境突然就體驗(yàn)不到這個(gè)功能了,無(wú)論輸入多少個(gè)@都不行,白天還好好的??。
好一個(gè)「白天還好好的」。
我自己測(cè)試的時(shí)候又百分百能體驗(yàn)到??,所以最開(kāi)始我還在懷疑他沒(méi)有配上測(cè)試系統(tǒng)......
于是,讓測(cè)試同學(xué)的windows電腦連到我的開(kāi)發(fā)環(huán)境debug一看:
好家伙,真是好家伙??他的電腦的e.key === "Process"????!!!
什么意思呢,就是正常我們理想中的@字符產(chǎn)生是shift+2按鍵的組合,監(jiān)聽(tīng)keydown之后我們會(huì)按順序收到兩個(gè)回調(diào):
e.key === "Shift",e.code === "ShiftLeft"或者shiftRighte.key === "@",e.code === "Digit2"
但是實(shí)際在測(cè)試同學(xué)的電腦里,1是一樣的,但是2變了,2變成了e.key === "Process"。
雖然鍵盤(pán)事件有變化,但是在前端頁(yè)面上的@字符是沒(méi)有任何變化的。難怪他說(shuō)他會(huì)突然失效了。我問(wèn)他做了什么怎么會(huì)突然變了,他想了想說(shuō)晚上從系統(tǒng)輸入法換成了微信輸入法.....
上網(wǎng)檢索(chatGPT)了一番,明白了一個(gè)新的知識(shí)點(diǎn):
輸入法的全稱叫Input Method Editor輸入法編輯器(IME)。本質(zhì)上它也是個(gè)編輯器。為了能輸入各類(lèi)字符(比如漢字和阿拉伯字等),IME會(huì)先處理用戶的英文字母輸入然后通過(guò)系統(tǒng)的底層調(diào)用傳遞給瀏覽器,瀏覽器再顯示到用戶的界面。這里的Process很大概率就是當(dāng)時(shí)輸入法給出的某個(gè)值表示那個(gè)時(shí)刻它還在處理中。
解決辦法
既然KeyBoardEvent靠不住,那我們換一種監(jiān)聽(tīng)方式。
我找到了一個(gè)非常適用于輸入法的監(jiān)聽(tīng)事件叫做CompositionEvent,它表示用戶間接輸入文本(如使用輸入法)時(shí)發(fā)生的事件。此接口的常用事件有compositionstart, compositionupdate 和 compositionend。它們?nèi)齻€(gè)事件分別對(duì)應(yīng)的動(dòng)作,通俗一點(diǎn)說(shuō)就是你用輸入法開(kāi)始打字、正在打字和結(jié)束打字。
于是乎,我監(jiān)聽(tīng)compositionend不就行了!在輸入法end的時(shí)候我再去看你end的字符是不是@不就行了!
addEventListner('compositionend', (e) => {
if (e.data === "@") {
e.preventDefalut();
}
});
對(duì)于輸入法來(lái)說(shuō),按鍵的up和down的key值就算不盡人意也沒(méi)什么損失,畢竟用戶毫無(wú)感知。但是,compositionend永遠(yuǎn)是不會(huì)錯(cuò)的,如果compositionend的e.data都不是@字符了,那么在用戶的編輯器界面上的顯示肯定也會(huì)跟著出錯(cuò)。
所以監(jiān)聽(tīng)這個(gè)肯定就是萬(wàn)無(wú)一失的方法了,哈哈哈我真是個(gè)“天才”(蠢材)。 修改之后讓測(cè)試同學(xué)嘗試之后果然就可以了。
坑3:輸入法繼續(xù)坑
起因
時(shí)間過(guò)去了沒(méi)一會(huì),本天才就收到了另一個(gè)測(cè)試同學(xué)反饋的問(wèn)題說(shuō)為什么輸入了一個(gè)@字符之后,會(huì)出現(xiàn)兩個(gè)@在界面上?
我第一反應(yīng)就是難道沒(méi)有執(zhí)行到e.preventDefalut()?既然后續(xù)功能能正常使用,沒(méi)執(zhí)行到也不應(yīng)該啊??。然后在我電腦一通嘗試,發(fā)現(xiàn)safari瀏覽器在輸入法為中文的情況下也會(huì)觸發(fā)這個(gè)問(wèn)題。
于是,讓測(cè)試同學(xué)的windows電腦連到我的開(kāi)發(fā)環(huán)境debug一看(梅開(kāi)二度):
執(zhí)行到了,也沒(méi)有報(bào)錯(cuò)什么的,但是@字符并沒(méi)有被prevent掉??。
再加上我自己傳入的@,所以界面上就出現(xiàn)了兩個(gè)@字符。啊這這這,這很難評(píng)......
我是左思右想,百思不得其解,于是只能:
stack overflow上也有這個(gè)問(wèn)題。
上面大概的意思就是compositionend事件里使用 e.preventDefault() 在技術(shù)上可行的,但它可能不會(huì)產(chǎn)生你期望的效果??赡苁且?yàn)?code style="font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.87em; word-break: break-word; border-radius: 2px; overflow-x: auto; background-color: rgb(255, 245, 245); color: rgb(255, 80, 44); padding: 0.065em 0.4em;"> compositionend 事件標(biāo)志著一次輸入構(gòu)成(composition)會(huì)話的結(jié)束,而在這個(gè)點(diǎn)上阻止默認(rèn)行為可能沒(méi)有意義,因?yàn)檩斎氲牧鞒桃呀?jīng)完成了。
更推薦用keydown,compositionstart和input來(lái)處理這種情況。
那keydown是不可能keydown了,已經(jīng)被坑了。compositionstart也不行,因?yàn)閯傞_(kāi)始輸入那會(huì)才按下了shift鍵,@字符還沒(méi)出來(lái)呢。那就只能input了。
解決辦法
最開(kāi)始我沒(méi)有選擇input就是因?yàn)樗荒苁褂?code style="font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.87em; word-break: break-word; border-radius: 2px; overflow-x: auto; background-color: rgb(255, 245, 245); color: rgb(255, 80, 44); padding: 0.065em 0.4em;">e.preventDefault()。我必須要對(duì)輸入的字符串進(jìn)行單獨(dú)處理,去掉@,當(dāng)時(shí)覺(jué)得很麻煩就沒(méi)有選擇這個(gè)方法。
額....好好好,行行行,現(xiàn)在還是必須得處理一下了。
addEventListner('input', (e) => {
if (e.data === "@") {
?怎么去處理字符呢
}
});
對(duì)于這個(gè)處理字符的方法,也是一個(gè)新知識(shí)點(diǎn)了。起初我還想的是去處理編輯器里的content,然后再給它插入回去,這樣子復(fù)雜度很高并且出錯(cuò)的概率極大。
這里的解決辦法主要是使用CharacterData接口?。CharacterData 接口是操作那些包含字符數(shù)據(jù)的節(jié)點(diǎn)的核心,特別是在需要?jiǎng)討B(tài)地更改文本內(nèi)容時(shí)。
例如,在一個(gè)文本節(jié)點(diǎn)上使用 deleteData() 方法可以從文本中移除一部分內(nèi)容,而不必完全替換或重寫(xiě)整個(gè)節(jié)點(diǎn)。
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}
寫(xiě)完這個(gè)之后,我用自己的safari瀏覽器測(cè)試發(fā)現(xiàn)果然沒(méi)有問(wèn)題了。
哈哈哈我真是個(gè)“天才”(蠢材)。
坑4:輸入法深坑???
我自信滿滿地讓測(cè)試同學(xué)再重試一下??,然后測(cè)試同學(xué)說(shuō):和之前一樣啊,還是有兩個(gè)@字符。
我:???啊啊??啊啊?????

于是,讓測(cè)試同學(xué)的windows電腦連到我的開(kāi)發(fā)環(huán)境debug一看(梅開(kāi)三度):
發(fā)現(xiàn)測(cè)試同學(xué)電腦上的anchorOffset和正常的情況下是不一樣的,會(huì)小一位,所以導(dǎo)致anchorOffset - 1拿到的前一個(gè)字符并不等于@,所以后續(xù)也沒(méi)有把它處理掉??。
我是左思右想,百思不得其解,stack overflow上也沒(méi)有相關(guān)的問(wèn)題。不過(guò),結(jié)合IME的概念,肯定還是輸入法的問(wèn)題。
結(jié)合之前keydown的e.key==="Processing",可能在input觸發(fā)時(shí)輸入法的編輯器其實(shí)還是沒(méi)有完成工作(composition),導(dǎo)致在那個(gè)時(shí)候Selection的anchorOffset不一致。其實(shí)瀏覽器的Selection肯定不會(huì)錯(cuò),那anchorOffset看起來(lái)像是錯(cuò)了,我覺(jué)得應(yīng)該是輸入法在轉(zhuǎn)換的過(guò)程對(duì)我們的前端頁(yè)面做了一些用戶看不到的東西,而anchorOffset把它顯化出來(lái)罷了。
解決辦法
于是乎,我嘗試性的對(duì)處理字符串的那串代碼進(jìn)行延時(shí),目的是為了等待輸入法徹底工作完畢。
addEventListner('input', (e) => {
if (e.data === "@") {
setTimeout(() => {
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}
});
}
});
然后,問(wèn)題真的就徹底解決了。
這個(gè)功能做起來(lái)可太簡(jiǎn)單了......??
轉(zhuǎn)自https://juejin.cn/post/7307041255740981286
該文章在 2025/11/6 10:26:10 編輯過(guò)