现在很多后台产品都喜欢做命令面板。
表面看起来,它像是一个很时髦的交互:
- 按下
Ctrl+K; - 弹出一个搜索框;
- 找到命令并执行。
但真正把它做稳定以后,你会发现难点根本不在这个输入框,而在这些问题:
- 命令从哪里来;
- 当前页面上下文会不会影响命令集合;
- 命令如何分组;
- 命名空间如何切换;
- 热键命令和搜索命令如何共存;
- 执行一个命令后,是关闭面板,还是切到下一层面板。
Odoo 的 command_service.js、command_palette.js、default_providers.js 给出了很完整的答案。
一、命令面板的核心不是面板,而是 commandService
commandService 依赖:
dialoghotkeyui
这已经很能说明问题。
它不是一个普通组件,而是一个运行时服务。
因为命令面板要同时管理:
- 打开方式;
- 当前活跃元素;
- 命令注册;
- 热键绑定;
- 对话框式展示;
- 面板开关状态。
所以它的本体当然不该只是“一个组件”。组件只是显示层,真正的组织者是 service。
二、Ctrl+K 只是入口,真正关键是 provider 汇聚
服务启动后,会给 control+k 绑定全局热键,调用 openMainPalette()。
而 openMainPalette() 并不是直接去搜索某个静态数组,它做了三件大事:
- 收集
command_provider注册表中的 provider; - 收集
command_categories里的分类元数据; - 收集
command_setup里的 placeholder、emptyMessage、debounceDelay 等配置。
这意味着 Odoo 的命令面板不是“服务里内置一份命令清单”,而是:
把命令来源、分类和显示配置全部注册化,再在打开时统一汇总。
这就给了系统极强的扩展性。
三、provider 解决的是“命令从哪里来”
default_providers.js 里至少能看到两类 provider:
1)命令注册型 provider
它会从 env.services.command.getCommands(...) 拿到已经注册的命令,再做:
- 分类校验;
isAvailable()过滤;- 去重;
- 包装成命令项组件。
2)页面热键型 provider
它会扫描当前活跃元素附近可见的:
[data-hotkey]:not(:disabled)
然后把页面里可触发的热键元素,也转换成命令面板中的候选项。
这点非常妙。
它意味着命令面板不是孤立宇宙,而是会把:
- 已注册的抽象命令;
- 页面上已有的可操作入口;
统一折叠到一个搜索体验里。
四、activeElement 让命令不是“全局平铺”,而是“带上下文过滤”
commandService.registerCommand() 里会记录 activeElement。
源码甚至专门等一个 microtask,再把当时的 ui.activeElement 作为命令订阅上下文。
这个细节很工程化。
因为命令的可用性,经常和“用户当前在哪块界面”有关。
举个最简单的例子:
- 某个表单中的命令,不该在别的页面也出现;
- 某个弹窗里的快捷动作,只在这个弹窗当前焦点上下文里有效;
- 同名命令可能在不同宿主元素下含义不同。
所以命令面板不是把全站命令粗暴铺平,而是会带着活跃上下文去取命令。
五、namespace 说明命令面板其实支持“模式切换”
command_palette.js 里很有意思的一段,是 processSearchValue():
- 如果输入首字符命中了某个 namespace,就切换 namespace;
- 例如
searchValue[0]对应 provider namespace。
随后面板会:
- 切换 placeholder;
- 应用该 namespace 的 debounceDelay;
- 只使用该 namespace 下的 provider。
这意味着命令面板不只是“搜一份总列表”,而是支持:
同一个壳,切换不同命令空间。
这比单纯在结果里加分类更进一步,因为 namespace 甚至能改变搜索语义和交互节奏。
六、命令执行后不一定关闭,可能进入下一层面板
executeCommand() 很关键:
- 如果
command.action()返回的是一个 config,就调用setCommandPaletteConfig(config); - 否则才关闭面板。
这说明 Odoo 的命令面板不是一次性跳转器,而是允许:
- 当前命令打开下一层选择;
- 从主面板切进某个子模式;
- 把命令面板当成一个逐步收窄的交互流程。
这正是很多产品做不出来的地方。
表面都叫命令面板,但有的只是“搜索后执行”,有的已经是“命令驱动的多步流程入口”。Odoo 显然偏后者。
七、Race、KeepLast 与 debounce 说明它很在意搜索并发
命令面板内部用了:
RaceKeepLastdebounce
这三个放在一起,说明作者非常清楚搜索 UI 的典型问题:
- 用户连续输入,旧请求晚回来把新结果覆盖;
- namespace 切换时旧搜索结果串回来;
- provider 异步返回顺序不稳定。
所以它不是“输入就查”,而是把并发控制也当成命令面板的一部分。
这点很重要,因为很多体验问题看起来像“搜索不准”,其实是结果回流顺序错了。
八、键盘交互不是附加功能,而是主链路
CommandPalette 在 setup 里直接绑定:
- Enter
- Control+Enter
- ArrowUp
- ArrowDown
再配合:
useAutofocus()- listbox 滚动同步
- 鼠标选择与键盘选择协调
这说明官方心里非常清楚:
命令面板的第一输入设备不是鼠标,而是键盘。
如果键盘导航是补丁式追加,命令面板就只能算“长得像命令面板”。
九、二开时最值得学的不是 UI,而是注册层次
很多团队做命令面板时,会把所有命令直接写进一个数组,然后组件里过滤。
短期能跑,长期会遇到:
- 命令越来越难拆模块;
- 页面上下文难注入;
- 不同业务组没法独立扩展;
- 分类、展示、命名空间全部缠成一团。
Odoo 的拆法更适合大项目:
- command service:总调度;
- provider:命令来源;
- category registry:分组元数据;
- setup registry:命名空间配置;
- palette component:展示与交互。
这是一种很典型的“扩展面向注册表,而不是面向 if/else”思路。
十、总结:命令面板本质上是命令运行时,不是搜索框
看完这套源码后,最值得记住的一句话是:
命令面板真正难的,从来不是把结果搜出来,而是把命令如何进入系统、如何带着上下文出现、如何继续切换模式这整套运行时组织好。
Odoo 的实现之所以耐看,就在于它把:
- 命令注册;
- 页面上下文;
- provider 汇聚;
- namespace 切换;
- 搜索并发控制;
- 键盘优先执行;
- 二级面板跳转;
全部当成同一个问题来解决。
所以它不是“一个 Ctrl+K 小弹层”,而是一套真正可扩展的命令系统。
DISCUSSION
评论区