前端

Odoo 热键为什么不会变成“全站抢键盘”:hotkey service、overlay 提示层与派发优先级讲透

热键系统最怕两件事:输入框误触,和多个组件互相抢快捷键。Odoo 的 `hotkey_service.js` 没有把快捷键做成一堆散落监听,而是做成了带作用域、可视提示和 DOM 回退的统一分发器。本文讲清它如何避免键盘交互失控。

前端
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

在复杂后台里做热键,很容易掉进两个坑:

  • 做少了,用户觉得操作慢;
  • 做多了,用户觉得键盘像被页面“抢走了”。

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 是一个很重要的刹车片

源码里只允许一组白名单按键进入分发流程,比如:

  • 字母数字;
  • 方向键;
  • entertabescape
  • 少量特殊符号。

这类白名单设计听起来保守,但在 ERP 场景反而非常必要。

因为后台页面里充满:

  • 原生浏览器快捷键;
  • 输入法候选操作;
  • 第三方控件自己的键盘行为;
  • 可编辑区域的文本输入。

Odoo 的策略不是“尽量接管更多键”,而是“先缩小可接管集合”。这能显著降低系统把键盘交互搞乱的概率。

三、editable 保护,才是让热键“像人话”的关键

onKeydown() 里有一段很值得所有前端团队抄作业的逻辑:

  • 如果目标元素是 inputtextareacontentEditable
  • 并且没有显式声明 data-allow-hotkeys
  • 那么除 escape 外,热键默认不抢。

这个判断表面很朴素,实战价值极高。

因为用户在输入框里敲键盘时,心理模型是“我在输入”,不是“我在对整个应用发命令”。

如果系统在这个时候还去抢:

  • ctrl+s
  • enter
  • tab
  • 字母键

用户会立刻觉得页面很冒犯。

所以 Odoo 的热键系统不是一上来强调能力,而是先尊重输入焦点的主权

四、Odoo 的分发不是平铺比较,而是“注册优先,DOM 回退”两层模型

dispatch() 会把候选源拼成两组:

  1. 通过 hotkeyService.add() 注册的逻辑热键;
  2. 通过 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 时不派发;
  • keyupblurclick 时移除 overlay;
  • 事件被真正处理时才 preventDefault()stopImmediatePropagation()

这说明官方很清楚:热键系统首先要像一个守规矩的浏览器公民,其次才是增强器。

八、源码里那些看似小众的细节,其实都对应真问题

例如:

  • 忽略数字小键盘某些输入,避免和 Windows ALT 编码冲突;
  • 非拉丁布局优先物理键位,避免国际键盘用户“按不到预期热键”;
  • withOverlay 让部分热键只在提示模式下生效,避免平时误触。

这些都不是学院派设计,而是长期在复杂业务前端里积累出来的“伤疤式工程经验”。

九、二开时最容易犯的错误

很多人自定义 WebClient 热键时,会直接在组件里监听 keydown。短期看简单,长期几乎一定出问题:

  • 焦点进输入框还抢键;
  • 弹窗打开后旧页面热键仍然在;
  • 下拉菜单与页面级热键冲突;
  • 浏览器默认行为被无差别阻断。

更稳的做法通常是:

  1. 能用 useHotkey() 就不要手搓 window 监听;
  2. 明确 areaglobal 边界;
  3. 除非特别需要,不要绕过 editable 保护;
  4. 面向可见操作项时优先用 data-hotkey / overlay 思路。

十、结论:Odoo 热键系统的核心不是“快”,而是“可裁决”

很多人把热键理解成效率插件,但在 Odoo 这种大前端里,真正难的是裁决冲突。

官方源码交出的答案很明确:

  • 先正规化按键;
  • 再按可编辑状态、UI 阻塞状态、作用域与优先级过滤;
  • 最后在逻辑注册和 DOM 声明之间做有序派发。

所以 Odoo 热键之所以没变成“全站抢键盘”,靠的不是运气,而是一整套分层约束。

这套约束,正是复杂 Web Client 能放心引入大量快捷键的前提。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。