中文输入法下的键盘事件陷阱

工具相关 ·

打字打到一半弹出了面板

给搜索框加一个快捷键:输入 / 就聚焦到搜索框,输入 Esc 就清空内容。测试时一切正常,上线后收到反馈——用户用中文输入法打字,打着打着页面就跳转了。

问题出在输入法的组字过程。中文、日文、韩文输入法在候选词上屏之前会有一个「组字」阶段,这个阶段里按键事件照常触发,但键值语义与最终输入结果完全不同。

组字阶段发生了什么

以拼音输入法为例,用户想输入「你好」,实际按键序列是 n i h a o,然后选词。在这五个字母键按下的过程中:

  • keydown 事件正常触发;
  • e.key 的值是 "Process",不是 "n"、"i" 这些字符;
  • e.code 仍然是真实的物理键位,如 KeyN;
  • 页面上输入框里显示的是输入法自己的候选框内容,input 事件可能还没触发。

如果此时代码用 e.key === '/' 或 e.code === 'Slash' 判断快捷键,就会把组字过程中的按键误当成快捷键,用户打字打到一半页面就跳走了。

用 composition 事件划出边界

浏览器为此提供了三个组合事件:compositionstart 在组字开始时触发,compositionupdate 在候选内容变化时触发,compositionend 在组字结束(上屏或取消)时触发。

维护一个状态标记,就能把快捷键逻辑挡在组字过程之外:

let composing = false;

input.addEventListener('compositionstart', () => composing = true);
input.addEventListener('compositionend',   () => composing = false);

document.addEventListener('keydown', (e) => {
  if (composing) return;          // 组字期间一律不处理快捷键
  if (e.key === 'Escape') { /* ... */ }
});

有一个细节要注意:不同浏览器的触发顺序不完全一致。Chrome 在 compositionend 之后还会再触发一次 keydown(e.key 是最终字符),而部分浏览器顺序相反。因此在 compositionend 里不要立刻把标记置回 false 并处理业务逻辑,最稳妥的做法是用 setTimeout(..., 0) 把处理推迟到当前事件循环之后,或者只依赖 keydown 里的标记判断。

判断「真的输入了内容」应该看哪里

组字过程中输入框的 value 已经包含了拼音串,所以不要用 value.length > 0 判断用户是否输入了有效内容。

正确做法是监听 input 事件并检查 e.isComposing:

input.addEventListener('input', (e) => {
  if (e.isComposing) return;      // 组字中,忽略
  updateResult(e.target.value);   // 真正输入完成
});

isComposing 是 InputEvent 上的标准属性,在组字期间为 true,比自己维护标记更可靠。如果需要兼容旧环境,可以退回到组合事件标记的方式。

别用 keypress 监听输入

keypress 在输入法组字期间根本不触发,用它统计输入、做即时搜索会漏掉中文输入;而且它已被规范标记为废弃。需要响应输入就用 input 事件,需要拦截按键就用 keydown。

同样要避免的还有「用 keydown 拼出输入内容」这类做法——拼出来的是按键序列,不是用户看到的文字,中文场景下结果完全对不上。

顺带一提:粘贴与拖拽也不走键盘事件

用户用 Ctrl+V 粘贴、拖拽文本进输入框,都不会产生字母键的 keydown。任何「输入完成后触发」的逻辑,都应该挂在 input 事件上,而不是挂在键盘事件上。这样键盘输入、粘贴、拖拽、语音输入、自动填充都能覆盖到。

检查清单

  • 快捷键处理是否在组字期间被跳过(isComposing 或组合事件标记)
  • 输入完成逻辑是否挂在 input 事件上,而不是 keydown / keypress
  • 是否还在用 keypress(应替换为 input 或 keydown)
  • compositionend 后的事件顺序是否处理正确(避免重复触发)
  • 粘贴、拖拽、语音输入这些非键盘输入路径是否也能触发逻辑
阅读 12