快捷键是那种「在自己电脑上测一遍就觉得做完了」的功能。而它失灵的场景,恰好都不在你的电脑上:用户是 Mac、用户用法语键盘、用户在打中文。
键码查询 能告诉你按下一个键时四个字段分别是什么。这篇讲的是拿到这些字段之后怎么用。
一、修饰键:Mac 和其他平台不是一个字段
const isMac = /Mac/i.test(
navigator.userAgentData?.platform || navigator.userAgent
);
function isMod(e) {
return isMac ? e.metaKey : e.ctrlKey;
}
三个常见错法:
❌ 用 || 连起来
if (e.ctrlKey || e.metaKey) { ... } // 错
这会让 Mac 上的 Ctrl+S 也触发。而 Ctrl 在 Mac 上有自己的含义(Ctrl+A 是行首、Ctrl+E 是行尾),占用它会破坏系统级的编辑习惯。
❌ 在 Windows 上用 Meta。Windows 的 Meta 键是 Win 键,被系统占着,按下去多半根本到不了网页。
❌ 不检查其余修饰键
if (isMod(e) && e.code === 'KeyS') { ... } // Ctrl+Shift+S 也会触发
if (isMod(e) && !e.shiftKey && !e.altKey && ...) { } // ✓
Ctrl+S 和 Ctrl+Shift+S 通常是「保存」和「另存为」,混在一起用户会很困惑。
二、key 还是 code:看你要的是字符还是位置
key | code | |
|---|---|---|
| 含义 | 打出来的字符 | 按下的物理位置 |
| 受布局影响 | 是 | 否 |
| 受 Shift 影响 | 是(2 → @) | 否 |
| 受输入法影响 | 是 | 否 |
用 code 的两类场景:
1. 游戏和方向控制。 WASD 在法语 AZERTY 键盘上那四个位置印的是 ZQSD,用 code 判断 KeyW/KeyA/KeyS/KeyD,法语用户不用改键位照样能玩。
2. 带修饰键的字母快捷键。 这一条更隐蔽:
// 俄语布局下按 Ctrl+S
e.key // "ы" ← 西里尔字母,不是 "s"
e.code // "KeyS" ✓
用 e.key === 's' 判断,俄语、希腊语、阿拉伯语用户的 Ctrl+S 完全收不到,而且他们不会来报 bug,只会觉得这个网站没有快捷键。
用 key 的场景:Enter、Escape、Tab、ArrowUp 这类功能键——语义明确、跨布局一致,用 code 反而啰嗦。以及用户「想按出那个字符」的场合,比如按 / 聚焦搜索框。
keyCode 一律不用。它已废弃,且在不同浏览器和布局下取值不一致,只在读老代码时有用——遇到 e.keyCode === 13 这种魔法数字,在 键码查询 里搜 13 就知道是 Enter。
三、输入法:那个 229
这是中文、日文、韩文用户会遇到而英文自测永远测不出来的一类 bug。
现象:用户打「你好」,按回车选词——同一下回车把表单也提交了,输入框里留着半截拼音。
原因:输入法在候选词阶段也会派发 keydown。这一次按键的本意是确认候选词,不是确认表单,但你的监听器收到了它。
if (e.isComposing || e.keyCode === 229) return; // 放在处理函数最前面
isComposing 是标准字段;keyCode === 229 是老浏览器上的表现,一起判更稳。
需要更精细的控制时,监听 compositionstart / compositionend 自己维护标志位:
let composing = false;
el.addEventListener('compositionstart', () => { composing = true; });
el.addEventListener('compositionend', () => { composing = false; });
凡是在输入框上绑回车的功能——搜索、发消息、提交——都要处理这一条。
四、焦点在哪:输入框里的取舍
function inEditable(e) {
const t = e.target;
return t instanceof HTMLElement && (
t.tagName === 'INPUT' ||
t.tagName === 'TEXTAREA' ||
t.tagName === 'SELECT' ||
t.isContentEditable
);
}
规则很简单:
- 单键快捷键(按
d删除、按/搜索)→ 焦点在可编辑元素里时一律跳过 - 带 Ctrl/Cmd 的组合(Ctrl+S 保存、Ctrl+Enter 提交)→ 要响应,用户在输入框里按这些正是他们的意图
还有一条无障碍要求值得知道:只用可打印字符做的单键快捷键,应该提供关闭或重新映射的选项,或者限定为「仅当某个元素获得焦点时生效」。依赖语音输入的用户会被单键快捷键反复误触发——他们说的每一个词都可能被当成一串快捷键。
五、e.repeat:按住不放
if (e.repeat) return; // 一次性动作要跳过
按住不放时浏览器会持续派发 keydown,e.repeat 为 true。
- 一次性动作(保存、提交、打开弹窗、切换状态)→ 跳过重复
- 连续动作(移动角色、滚动、微调数值)→ 正需要它
漏掉这一条的表现是:用户手指在键盘上停了半秒,弹窗打开了二十次。
六、preventDefault 的时机
只在自己确实处理了这次按键之后才调用。
if (isMod(e) && !e.shiftKey && e.code === 'KeyS') {
e.preventDefault(); // 先拦掉浏览器的「保存网页」
save();
return;
}
// 没匹配上,什么都不做
有几个组合不要去拦:Cmd+R / F5(刷新)、Ctrl+W(关标签)、Ctrl+T(新标签)、Cmd+Q。拦掉它们,用户的第一反应是浏览器坏了,代价远大于收益。
完整骨架
const isMac = /Mac/i.test(navigator.userAgentData?.platform || navigator.userAgent);
document.addEventListener('keydown', (e) => {
// 1. 输入法组字中
if (e.isComposing || e.keyCode === 229) return;
// 2. 自动重复
if (e.repeat) return;
const mod = isMac ? e.metaKey : e.ctrlKey;
const edit = e.target instanceof HTMLElement &&
(/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) || e.target.isContentEditable);
// 3. 带修饰键的组合:字母用 code
if (mod && !e.shiftKey && !e.altKey && e.code === 'KeyS') {
e.preventDefault();
save();
return;
}
// 4. 单键快捷键:输入框里不响应
if (edit) return;
// 5. 功能键用 key
if (e.key === 'Escape') { closeDialog(); return; }
if (e.key === '/') { e.preventDefault(); focusSearch(); return; }
});
自测清单
在自己机器上能测的:
- 输入框里打字,单键快捷键不触发
- 按住某个快捷键不放,动作只发生一次
- Ctrl+Shift+S 不会触发 Ctrl+S 的逻辑
- 浏览器自己的刷新、关标签仍然正常
需要额外环境的(但影响最大):
- 切到中文输入法,在输入框里打字按回车——只选词,不提交
- Mac 上用 Cmd,且 Ctrl 组合不误触发
- 系统里临时加一个非拉丁布局(俄语最快),确认字母快捷键仍然工作
最后两条是最容易跳过、也最容易出事的。按一下 键码查询 的捕获区,能直接看到当前布局下 key 和 code 分别是什么——切换布局前后各按一次,差别一目了然。