写一套不会在别人电脑上失灵的快捷键:Cmd/Ctrl、布局、输入法

· 约 5 分钟 ⌨️ 键码查询

快捷键是那种「在自己电脑上测一遍就觉得做完了」的功能。而它失灵的场景,恰好都不在你的电脑上:用户是 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:看你要的是字符还是位置

keycode
含义打出来的字符按下的物理位置
受布局影响
受 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 的场景EnterEscapeTabArrowUp 这类功能键——语义明确、跨布局一致,用 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;   // 一次性动作要跳过

按住不放时浏览器会持续派发 keydowne.repeattrue

  • 一次性动作(保存、提交、打开弹窗、切换状态)→ 跳过重复
  • 连续动作(移动角色、滚动、微调数值)→ 正需要它

漏掉这一条的表现是:用户手指在键盘上停了半秒,弹窗打开了二十次。

六、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 组合不误触发
  • 系统里临时加一个非拉丁布局(俄语最快),确认字母快捷键仍然工作

最后两条是最容易跳过、也最容易出事的。按一下 键码查询 的捕获区,能直接看到当前布局下 keycode 分别是什么——切换布局前后各按一次,差别一目了然。

❓ 常见问题

判断 Ctrl / Cmd 该写哪个字段?

Mac 看 metaKey,Windows 和 Linux 看 ctrlKey,封装成一个变量写法const mod = isMac ? e.metaKey : e.ctrlKey;,之后统一判断 mod平台怎么判navigator.platform 已废弃,优先用 navigator.userAgentData?.platform,取不到再退回 UA 里找 Mac三个容易漏的点:(1) 不要把 ctrlKeymetaKey|| 连起来 —— 那会让 Mac 上的 Ctrl+S 也触发,而 Ctrl+S 在 Mac 上是别的含义;(2) Windows 的 Meta 键是 Win 键,被系统占用,不要拿它当修饰键;(3) 要精确匹配组合 —— 想响应 Ctrl+S 就该确认 !e.shiftKey && !e.altKey,否则 Ctrl+Shift+S 也会触发,而那通常是「另存为」。

keycode 到底什么时候用哪个?

记住一句:key 是「打出了什么字符」,code 是「按了键盘上哪个位置」code 的场景:(1) 游戏的 WASD ——法语 AZERTY 键盘上那四个位置印的是 ZQSD,用 code 照样能玩;(2) 带修饰键的字母快捷键 —— 俄语、希腊语等非拉丁布局下,key 是西里尔字母而不是 sCtrl+Skey 判断会完全收不到,用 e.code === "KeyS" 才稳。key 的场景:(1) Enter、Escape、Tab、方向键这类功能键,语义明确且跨布局一致;(2) 用户「想按出那个字符」的场合,比如按 / 聚焦搜索框。别用 keyCode —— 已废弃,且在不同浏览器和布局下取值不一致,只在读老代码时用得上,可以在 键码查询 里按数字反查。

为什么中文输入法下按回车会「既选词又提交表单」?

因为组字期间的那次 keydown 也被你的代码收到了机制:输入法在候选词阶段会派发 keydown,此时 event.isComposingtrue(老浏览器上表现为 keyCode === 229),这一次按键的本意是确认候选词,不是确认表单。不处理的后果:用户打「你好」按回车选词,同一下回车又把表单提交了,输入框里只留下半截拼音——中文用户最常遇到的一类 bug,而只用英文自测永远发现不了。正确写法if (e.isComposing || e.keyCode === 229) return; 放在处理函数最前面。配套:需要更精细控制时监听 compositionstart / compositionend 自己维护一个标志位。日文、韩文输入法同理

输入框里该不该响应全局快捷键?

单键快捷键一律不响应,带修饰键的通常要响应规则:处理函数开头先判断焦点在哪——e.targetinputtextareaselect 或带 contenteditable 的元素时,跳过所有单键快捷键,否则用户打字打出一个 d 就触发了删除。带 Ctrl/Cmd 的组合要放行:Ctrl+S 保存、Ctrl+Enter 提交,用户在输入框里按这些正是他们的意图。还有一条无障碍要求:只用可打印字符做的单键快捷键,应当提供关闭或重新映射的选项,或者限定为「仅在某个元素获得焦点时生效」——依赖语音输入的用户会被单键快捷键反复误触发。长按也要处理e.repeattrue 时跳过一次性动作,否则按住不放会连续触发几十次。

⌨️ 打开 键码查询 按一下显示 key/code/keyCode·修饰键组合·常用键码表可搜

🔗 相关阅读

全部教程 →