打字打到一半弹出了面板
给搜索框加一个快捷键:输入 / 就聚焦到搜索框,输入 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后的事件顺序是否处理正确(避免重复触发)- 粘贴、拖拽、语音输入这些非键盘输入路径是否也能触发逻辑