当初实现组合快捷键功能的时候,觉得这是个并不复杂的功能,同时结合项目自身的特殊需要,并没有考虑引入第三方解决方案,于是乎自己简单实现了一个,keydown/keyup 事件安排起来,直到有一天发现有时候快捷键会失效,比如当我使用系统快捷键切换窗口,在切回时,快捷键就失效了。
原因
调试发现,当我通过快捷键切换窗口时,比如 ctrl+arrowright
,会导致 ctrl keydown 事件触发, 但 keyup 事件并没有触发,从而导致我代码中维护的 pressedKeys Set 数据不对,从而导致无法正确触发。
所以这里我犯了一个错误,那就是 keydown 事件触发,并不代表 keyup 事件一定会触发,比如当你绑定事件元素失去焦点时,再松开按键,此时 keyup 事件就不会触发。
详细理解下三个事件
- keydown:在控件有焦点的情况下按下键时发生
- keypress:在控件有焦点的情况下按下键时发生
- keyup:在控件有焦点的情况下释放键时发生
他们的前提是控件有焦点,但当你松开按键之前,你是有可能让控件失去焦点的。
keypress 和 keydown 的区别
- keypress:keydown 比较底层,而 keypress 比较高级,它只有在按下能产生字符的按键时才会触发,比如字母、数字、标点符号等,对于修饰符按键、功能按键则不会触发。查阅 mdn 提示该事件已经过时,不推荐再使用。
- keydown:可以捕获所有按键,不区分字母大小写,而 keypress 区分
keydown、keypress 按着不动会持续执行事件,keyup 执行一次
Mousetrap
于是乎,我就很好奇第三方库是如何解决这个问题的,比如 MouseTrap。
查阅后,MouseTrap 不同于我的实现方式,并没有通过类似 pressedKeys 记录按键按下情况,仅仅是通过当前的 event 对象确定回调函数,因此猜测只能实现修饰符按键组合单个字符的组合方式,无法实现类型 ctrl+a+b
的快捷键组合,经验证,的确如此!
Mousetrap 使用注意点
- 如果你在脚本中绑定相同的键事件,它会覆盖你指定的原始回调。回调函数中返回 false,会阻止浏览器默认行为和冒泡
- 每个实例都跟踪自己的回调,如果将相同的键绑定到多个元素,所有单独的回调都将触发,除非使用 event.stopPropagation()
KeyboardJS
KeyboardJS 相比 Mousetrap 而言体积要大很多,但提供了很多其他的特性
- 兼容 nodejs
- 提供上下文概念,对于单页应用而言十分有用,允许将绑定范围限定到应用程序的各个部分
- 支持更复杂的绑定,例如
a+b
那么为啥它可以实现 a+b
的逻辑呢,简单翻阅下源代码,它同样通过 pressedKeys 记录按下的值,那么为啥它不存在我这里的问题呢。
看到如下一段代码
this._bindEvent(targetElement, 'keydown', this._targetKeyDownBinding);
this._bindEvent(targetElement, 'keyup', this._targetKeyUpBinding);
// Notice: here is window object
this._bindEvent(targetWindow, 'focus', this._targetResetBinding);
this._bindEvent(targetWindow, 'blur', this._targetResetBinding);
解决方式是监听 focus
和 blur
事件,当触发时,清空所有的按键,这样导致的结果是,你需要松开当前按键,然后重新按下,但这无伤大雅。
还看到一个我自己实现时踩的坑的解决方法如下
this._targetKeyDownBinding = (event) => {
this.pressKey(event.keyCode, event);
this._handleCommandBug(event, platform);
}
_handleCommandBug(event, platform) {
// On Mac when the command key is kept pressed, keyup is not triggered for any other key.
// In this case force a keyup for non-modifier keys directly after the keypress.
const modifierKeys = ["shift", "ctrl", "alt", "capslock", "tab", "command"];
if (platform.match("Mac") && this._locale.pressedKeys.includes("command") &&
!modifierKeys.includes(this._locale.getKeyNames(event.keyCode)[0])) {
this._targetKeyUpBinding(event);
}
}
处理方式是当你按下非修饰符键时(已交给 pressKey 处理完后),如果 command 已经被按下,代码里面手动释放掉该键,这样的后果是,cmd+c
已经触发,但 cmd+c+v
已经就无法触发了(未验证)
其他坑
已知问题:在 Mac 上,command 键按下时,非修饰符按键的 up 事件不触发,比如 cmd+z,松开 z,此时 up 事件不会触发。可以借鉴 KeyboardJS 的处理方式。