背景
大家好,我是 Canmick。前段时间在公司开发了一个 AI 对话机器人,刚上线就收到了一个 Bug,说是输入中文时按回车键会直接发送对话。
看到描述的一瞬间,觉得是中文输入法兼容性问题,因为代码里使用的是原汁原味的 Vue 键盘事件修饰符 @keydown.enter,不应该有问题呀。
带着疑问 debugger 了一下代码,果然...与输出法无关,不管输入法是否键入中,只要按下回车键就会触发 @keydown.enter ,好低级的错误呀,接着看看如何处理。
问题分析
使用英文输入法时,正常键入字母符号不会弹出文字选框,此时键入和回车暂未发现冲突。
使用中文输入法时,以拼音为例,键入拼音时会出现文字选框,一般以空格键确认文字选择,此时键入与回车暂未发现冲突;
但存在文字选框时直接按下回车键,冲突就来了,正在输入的拼音会以字母形式键入输入框,同时触发输入框的回车事件,问题复现。
核心问题是中文输入法的文字选框回车事件和输入框回车事件冲突,解决思路就是触发输入框回车事件时,判断是否处于文字选框状态,是就不执行输入框的回车事件逻辑。
那这个输入法的文字选框是个什么东西?又如何判断是否处于文字选框呢?
其实这个文字选框又叫输入法编辑器 Input Method Editor,简称 IME,可以将用户多个输入单元合成为目标文字。
以下为 MDN 的描述,可以看出不仅中文需要IME,日文、韩文、拉丁字也需要。
至于 IME 状态,HTML 规范提供了相关接口,分别是
-
CompositionEventcompositionstartcompositionupdatecompositionend
-
KeyboardEventisComposing
前因后果已然清晰,事不宜迟,开始编码。
解决方案
判断IME最省事的方法就是 KeyboardEvent 的 isComposing,以此展开
方案1:在每个事件回调中添加判断
- 优点:快速解决问题上线
- 缺点:无法复用
const handleKeydown = (e) => {
if(e.isComposing) return;
// other logic
}
方案2:自定义 Input 组件
- 优点:较高复用性
- 缺点:旧业务较替换新组件存在一定成本
<script setup lang="ts">
defineOptions({
name: 'MyInput'
})
const emit = defineEmits(['enter'])
// 必须是 keydown 事件,keyup.enter 的 isComposing 已经为 false
const handleKeydownEnter = (e: KeyboardEvent) => {
if (!e.isComposing) {
emit('enter', e)
}
}
</script>
<template>
<input type="text" @keydown.enter="handleKeydownEnter" />
</template>
// 使用
<MyInput @enter="handleEnter"/>
方案2:自定义指令 v-enter
- 优点:较灵活
- 缺点:...
export const VEnterDirective = {
// 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
mounted(el, binding) {
el._enterCallback = (e) => {
if (e.keyCode === 13 && !e.isComposing && typeof binding.value === 'function') {
binding.value(e)
}
}
el.addEventListener('keydown', el._enterCallback)
},
// 绑定元素的父组件卸载前调用
beforeUnmount(el) {
el.removeEventListener('keydown', el._enterCallback)
delete el._enterCallback
}
}
// 注册
const app = createApp({})
app.directive('enter', VEnterDirective)
// 使用
<input type="text" v-enter="handleKeydownEnter" />
小结
输入法输入时弹出的悬浮窗口叫做输入法编辑器IME,主要作用是合成文字,例如合成中文、日文、韩文、拉丁字等。
vue 的@keydown.enter 不会判断当前是否存在IME,导致中文输入过程按回车键时,@keydown.enter也会同时触发。
解决该问题可以利用 KeyboardEvent 的 isComposing,更优方案可以封装 Input 组件暴露 @enter 事件或封装自定义指令 v-enter。