前端

Odoo hooks 为什么不是“顺手封个函数”:useService、useAutofocus、usePager 的生命周期契约讲透

Odoo 里的 hooks 看起来很轻,但真正的价值不是少写几行代码,而是把组件生命周期、环境注入和异步安全封成契约。结合 hooks.js 与 pager_hook.js,本文专门讲透 useService、useAutofocus、usePager 分别在保护什么,以及为什么它们不能简单当工具函数看待。

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

很多人第一次看 Odoo 的 hooks,会觉得它们只是一些“顺手封装”:

  • useService() 不就是取一下 env.services
  • useAutofocus() 不就是组件出来后聚焦一下;
  • usePager() 不就是把翻页参数传给 pager。

如果只这么理解,会很容易低估 Odoo hooks 的真正价值。

addons/web/static/src/core/utils/hooks.jsaddons/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() 像个小工具。

但源码里处理了几层现实约束:

  1. 默认跳过 touch 设备与移动系统,避免虚拟键盘突兀弹出;
  2. 检查当前元素是否真的在“可安全聚焦”的活跃区域里;
  3. 若是 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 一个值,而是悄悄挂进:

  • useEffect
  • useState
  • useSubEnv
  • 生命周期回调
  • 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

评论区

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