在复杂后台里做热键,很容易掉进两个坑:
- 做少了,用户觉得操作慢;
- 做多了,用户觉得键盘像被页面“抢走了”。
Odoo WebClient 恰好是最容易出事的那种场景:
- 有表单、有弹窗、有搜索框;
- 有下拉菜单、有全局导航、有 iframe 或 overlay;
- 同一个按键在不同区域还可能代表不同动作。
如果只是到处 window.addEventListener("keydown", ...),系统迟早会失控。
顺着 addons/web/static/src/core/hotkeys/hotkey_service.js 看源码,你会发现 Odoo 真正在做的不是“定义一堆快捷键”,而是建立一个统一热键裁决器。
同一时刻,哪个热键在什么区域、面对什么焦点元素、以什么优先级生效,都要被显式判断。
一、第一步不是执行,而是把用户按下的键正规化
getActiveHotkey(ev) 做的事,比看起来细得多。
它会处理:
- Mac 和 Windows 上修饰键语义差异;
Space的命名统一;- 顶部数字键与小键盘数字键区别;
- 非拉丁键盘布局下,用物理键位回退;
- 输入法组合态
isComposing时直接跳过。
这说明 Odoo 不是把热键当“字符串比较”,而是先把浏览器键盘事件转成一个稳定表示。
没有这一步,后面所有注册、优先级和冲突管理都会非常脆弱。
二、AUTHORIZED_KEYS 是一个很重要的刹车片
源码里只允许一组白名单按键进入分发流程,比如:
- 字母数字;
- 方向键;
enter、tab、escape;- 少量特殊符号。
这类白名单设计听起来保守,但在 ERP 场景反而非常必要。
因为后台页面里充满:
- 原生浏览器快捷键;
- 输入法候选操作;
- 第三方控件自己的键盘行为;
- 可编辑区域的文本输入。
Odoo 的策略不是“尽量接管更多键”,而是“先缩小可接管集合”。这能显著降低系统把键盘交互搞乱的概率。
三、editable 保护,才是让热键“像人话”的关键
onKeydown() 里有一段很值得所有前端团队抄作业的逻辑:
- 如果目标元素是
input、textarea或contentEditable; - 并且没有显式声明
data-allow-hotkeys; - 那么除
escape外,热键默认不抢。
这个判断表面很朴素,实战价值极高。
因为用户在输入框里敲键盘时,心理模型是“我在输入”,不是“我在对整个应用发命令”。
如果系统在这个时候还去抢:
ctrl+sentertab- 字母键
用户会立刻觉得页面很冒犯。
所以 Odoo 的热键系统不是一上来强调能力,而是先尊重输入焦点的主权。
四、Odoo 的分发不是平铺比较,而是“注册优先,DOM 回退”两层模型
dispatch() 会把候选源拼成两组:
- 通过
hotkeyService.add()注册的逻辑热键; - 通过 DOM 上
data-hotkey暴露的元素热键。
并且注册热键的优先级更高,遍历时还按“后注册先匹配”。
这个策略很聪明。
为什么先看服务级注册
因为组件逻辑最清楚当前上下文,知道:
- 是否允许重复触发;
- 只在某一块区域生效还是全局生效;
- 当前目标元素是否满足条件;
- 是否要绕过 editable 保护。
为什么还保留 DOM 级 data-hotkey
因为很多导航、菜单、按钮,本质上就是“当前可见元素上直接标一个可触发键”。
例如 navbar、dropdown 菜单这种 UI,DOM 声明式热键非常合适。
这就形成了 Odoo 的热键哲学:
- 复杂逻辑,用服务注册;
- 可见操作项,用 DOM 声明;
- 服务优先,DOM 补位。
五、区域(area)机制,避免“同键不同义”互相打架
热键注册支持 area(),并且在多个候选都匹配时,会挑更“近”的那个区域。
这意味着同样一个按键,可以在:
- 整个 WebClient;
- 某个 dropdown;
- 某个 popover;
- 某个面板;
拥有不同的含义,但最终只会由当前最相关那块 UI 获胜。
这是大系统里热键可维护的关键。
如果没有 area 语义,你只能靠:
- 手工开关监听器;
- 到处 stopPropagation;
- 或者把热键写得越来越保守。
Odoo 直接把“最近作用域优先”做进裁决器,复杂度一下降低很多。
六、overlay 提示层不是装饰,而是热键 discoverability 方案
hotkeyService.overlayModifier = "alt",并且在只按 overlay modifier 时,会展示热键提示层。
同时源码还会把原生 accesskey 转成 data-hotkey,接管默认行为。
这背后其实在解决一个经典矛盾:
- 快捷键很强,但用户记不住;
- 如果所有热键都藏着,等于没设计。
Odoo 的做法是:
- 平时不强迫用户背诵;
- 按住 overlay modifier 时,把当前可见可触发项提示出来。
这样热键就不再只是“高手专属暗门”,而变成一种可被发现的增强交互。
七、为什么它还要特判 UI blocked、blur、click、keyup
热键系统最常见的 bug,不是按下去没反应,而是“提示层收不掉”“模态框关了热键还残留”“界面阻塞时还能误触”。
所以 Odoo 还做了几件很实际的事:
ui.isBlocked时不派发;keyup、blur、click时移除 overlay;- 事件被真正处理时才
preventDefault()与stopImmediatePropagation()。
这说明官方很清楚:热键系统首先要像一个守规矩的浏览器公民,其次才是增强器。
八、源码里那些看似小众的细节,其实都对应真问题
例如:
- 忽略数字小键盘某些输入,避免和 Windows ALT 编码冲突;
- 非拉丁布局优先物理键位,避免国际键盘用户“按不到预期热键”;
withOverlay让部分热键只在提示模式下生效,避免平时误触。
这些都不是学院派设计,而是长期在复杂业务前端里积累出来的“伤疤式工程经验”。
九、二开时最容易犯的错误
很多人自定义 WebClient 热键时,会直接在组件里监听 keydown。短期看简单,长期几乎一定出问题:
- 焦点进输入框还抢键;
- 弹窗打开后旧页面热键仍然在;
- 下拉菜单与页面级热键冲突;
- 浏览器默认行为被无差别阻断。
更稳的做法通常是:
- 能用
useHotkey()就不要手搓 window 监听; - 明确
area和global边界; - 除非特别需要,不要绕过 editable 保护;
- 面向可见操作项时优先用
data-hotkey/ overlay 思路。
十、结论:Odoo 热键系统的核心不是“快”,而是“可裁决”
很多人把热键理解成效率插件,但在 Odoo 这种大前端里,真正难的是裁决冲突。
官方源码交出的答案很明确:
- 先正规化按键;
- 再按可编辑状态、UI 阻塞状态、作用域与优先级过滤;
- 最后在逻辑注册和 DOM 声明之间做有序派发。
所以 Odoo 热键之所以没变成“全站抢键盘”,靠的不是运气,而是一整套分层约束。
这套约束,正是复杂 Web Client 能放心引入大量快捷键的前提。
DISCUSSION
评论区