很多人第一次看 Odoo 的 hooks,会觉得它们只是一些“顺手封装”:
useService()不就是取一下env.services;useAutofocus()不就是组件出来后聚焦一下;usePager()不就是把翻页参数传给 pager。
如果只这么理解,会很容易低估 Odoo hooks 的真正价值。
看 addons/web/static/src/core/utils/hooks.js 和 addons/web/static/src/search/pager_hook.js 就会发现,官方在 hooks 里反复解决的是同一类问题:
组件生命周期变化很快,但共享能力、异步调用和页面状态必须保持稳定。
所以这些 hooks 的本质不是“帮你省代码”,而是把运行时契约提前封好。
一、useService() 的重点不是拿服务,而是避免“组件死了,异步还在回调”
很多框架里,从 context 拿 service 看起来只是依赖注入。
Odoo 更进一步。
useService() 在服务带有元信息时,会通过 _protectMethod() 包一层受保护的方法调用。核心逻辑非常明确:
- 调用服务方法时,如果组件已经 destroyed,就直接走兜底处理;
- 即便 promise 已经发出,返回结果时也会再次检查组件状态;
- 如果组件已销毁,就让 promise 悬空,不再把结果回灌到已死亡组件。
这段设计特别像在回答一个真实工程问题:
- 页面切走了;
- 组件销毁了;
- RPC 或 service promise 才回来;
- 结果又去 setState、弹提示、继续联动。
如果没有这层保护,前端就会出现很典型的幽灵更新:
- 报错;
- 内存泄漏;
- 已离开的页面突然被旧回调“补一刀”。
所以 useService() 真正提供的,不只是访问入口,而是组件生命周期感知的服务调用边界。
二、useAutofocus() 真正处理的是 UI 所有权,而不是聚焦本身
从 API 名字看,useAutofocus() 像个小工具。
但源码里处理了几层现实约束:
- 默认跳过 touch 设备与移动系统,避免虚拟键盘突兀弹出;
- 检查当前元素是否真的在“可安全聚焦”的活跃区域里;
- 若是 input/textarea,还会控制光标落点与是否全选。
特别值得注意的是 isFocusable() 那段逻辑。它不是简单判断元素存在,而是结合 ui service 的 activeElement 去判断:
- 当前组件是不是正处在活动 UI 容器内;
- Shadow DOM 场景下宿主关系是否仍然成立。
这说明 Odoo 对“自动聚焦”的理解不是“DOM 一出现就抢焦点”,而是:
只有当当前界面真正拥有交互所有权时,自动聚焦才合理。
这和弹窗、popover、文件查看器、自动补全等场景特别相关。否则你很容易把用户当前正在操作的焦点抢走。
三、usePager() 不是为了少传 props,而是把分页变成页面环境能力
usePager() 的实现非常短,但恰恰因为短,意思更明显。
它做了三件事:
- 读取当前 env;
- 创建一个响应式
pagerState; - 用
useSubEnv()把config.pagerProps注入到子环境; - 在
onWillRender()时把最新分页参数同步进去。
这意味着 Odoo 的分页不是“某个局部 renderer 私有的小状态”,而是页面壳层可以读取的一份环境能力。
为什么要这样做?
因为 Control Panel 往往不在数据 renderer 的直接子链上。若分页只在局部组件里保存:
- 列表区知道 offset/limit;
- 但上方控制面板拿不到;
- 最终 UI 就会被迫走很脆弱的跨层传参。
usePager() 通过 sub env 解决的是共享状态的结构性位置问题。
四、这三个 hook 共同指向一个设计哲学:把“危险细节”收进框架层
把它们放在一起看,会更容易看出 Odoo 的思路:
useService()处理异步结果与组件死亡的竞态;useAutofocus()处理焦点所有权与设备差异;usePager()处理共享状态如何穿过页面壳层。
它们表面上分属不同问题,底层却在做同一件事:
让业务组件拿到的是“可直接使用的能力”,而不是一堆自己兜底的危险细节。
这也是为什么 Odoo 的 hook 经常不只是 return 一个值,而是悄悄挂进:
useEffectuseStateuseSubEnv- 生命周期回调
- UI service
这些底层机制。
五、为什么不能把它们随手改写成普通 helper
很多二开项目会犯一个错误:
- 觉得 hook 太轻;
- 自己写个 helper 替代;
- 最终只保留“表面功能”,丢掉生命周期契约。
比如:
- 自己直接
this.env.services.xxx,失去 destroyed 保护; - 自己
setTimeout(() => input.focus()),失去 active UI 边界; - 自己把分页 props 往下传,失去页面壳层共享能力。
短期可能一样能跑,长期维护成本却完全不同。
六、实战上应该怎么用
如果你在写 Odoo 前端组件,我更建议把这些 hook 当成架构接口而不是工具箱:
- 需要共享运行时能力,优先想
useService(); - 需要自动聚焦,先确认是否真拥有当前交互上下文,再用
useAutofocus(); - 需要让 Layout / Control Panel / 页面区共享分页状态,就用
usePager()进 env,不要乱传 props。
结语
Odoo hooks 的厉害之处,不在于“封装得优雅”,而在于它们把最容易踩坑的运行时问题提前吸收掉了。
所以更准确的理解应该是:
useService()是异步安全契约;useAutofocus()是焦点所有权契约;usePager()是页面共享状态契约。
当你这样看它们时,就不会再把 Odoo hooks 当成“顺手封个函数”了。
DISCUSSION
评论区