前端

Odoo 前端为什么不是“组件互相 import 就完了”:registry、service 启动顺序与扩展边界讲透

很多人刚看 Odoo Web 前端,会觉得它不过是 OWL 组件加一堆 import。可官方源码真正稳定扩展的核心,不是硬耦合引用,而是 registry 和 service 的分层协作。本文结合 web/core/registry.js 与 env.js,讲清 Odoo 前端为什么能在大规模模块扩展下保持可插拔。

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

很多人第一次看 Odoo 前端源码,最容易产生一个错觉:

这不就是 OWL 组件 + import / export 吗?

但只要你继续往 addons/web/static/src/core/registry.jsaddons/web/static/src/env.js 里看,就会发现官方真正想解决的问题不是“怎么把组件写出来”,而是:

  1. 不同模块如何在不直接互相依赖的前提下扩展同一套 Web Client
  2. 一堆前端能力如何按依赖顺序稳定启动
  3. 后来加载的新模块,怎么还能继续挂进系统里而不把整台前端机器拆掉重装

这时候,registryservice 才是主角。

一、registry 不是“全局对象”,而是可排序、可校验、可监听的扩展总线

registry.js 开头就把定位写得很直白:它本质上是一个 string -> object 的映射。

但 Odoo 没停在“字典”这一步,而是额外给了三层能力:

  • 唯一键约束:重复 key 直接抛 DuplicatedKeyError
  • 取值失败显式报错:拿不到 key 会抛 KeyNotFoundError
  • 顺序控制:每个 entry 带 sequencegetAll() / getEntries() 会按序输出
  • 变更通知:Registry 继承 EventBus,有 UPDATE 事件
  • 结构校验:debug 模式下可挂 schema 验证

这就意味着,Odoo 的 registry 不是“随便往全局桶里丢点东西”,而是一个带约束的插件槽位系统。

为什么 sequence 很关键

很多人只注意到 add(key, value),却忽略了 sequence

Odoo 不是简单看“谁先 import 谁先赢”,而是允许多个模块往同一类 registry 里注册能力,再由 sequence 决定最终顺序。这样做的意义很大:

  • 菜单项、field widget、systray、命令、视图扩展都能稳定排序;
  • 扩展模块不需要篡改原模块源码,只要插到合适位置;
  • 多模块并存时,顺序不依赖打包偶然性。

这就是 Odoo 前端可插拔的第一层基础:扩展先进入 registry,再由 registry 决定系统如何看见它们。

二、sub registry 说明 Odoo 不是一个 registry,而是一棵扩展树

registry.category("services") 这种写法非常关键。

它说明官方没有做成一个扁平大对象,而是做成了一个按能力域拆分的子注册表树。常见的类别包括:

  • services
  • fields
  • views
  • main_components
  • effects
  • public.interactions

这背后的设计意图非常清楚:

  • 同一条扩展机制统一心智模型
  • 不同类型能力各自独立演化
  • 模块作者只需要知道自己该挂到哪个 category

所以 Odoo 前端扩展并不是“每个子系统都发明一套新接口”,而是尽量把入口统一到 registry 这套约定里。

三、service 才是 Web Client 的运行时骨架,不是普通工具函数

env.js 里最重要的不是 makeEnv(),而是 startServices(env)

这里能看出 Odoo 对 service 的理解并不是“一个 utils 模块”,而是有生命周期、可依赖、可异步启动的运行时能力

源码里给 services registry 加了 validation:

  • 必须有 start
  • 可以声明 dependencies
  • 可以声明 async

这意味着一个 service 的定义,不只是“暴露几个函数”,而是在声明:

  1. 我启动时需要哪些别的 service;
  2. 我自己是不是异步服务;
  3. 我何时才算真正可用。

所以像 notification、dialog、action、orm、ui 这些能力,不是随便 import 一个单例出来就结束了,而是被放进统一的服务启动流程里。

四、Odoo 为什么要专门做依赖解析,而不是按 import 顺序启动

startServices 里有一段非常值得注意:

  • 它先收集所有待启动 service;
  • 再不断找出“依赖已经满足”的 service;
  • 能并行启动的就并行启动
  • 启动完一批后再继续找下一批;
  • 如果最后还有服务没法启动,就汇总缺失依赖后报错。

这套逻辑的意义是:

1)避免硬编码启动顺序

如果全靠 import 顺序,那模块一多就会非常脆弱。谁先打包、谁先执行、谁先 side effect,都会变成潜在炸点。

而依赖解析改成:

不是“你写在前面就先跑”,而是“你依赖满足了才能跑”。

这样系统稳定性高很多。

2)允许最大程度并行

Odoo 不是串行把所有 service 一个个启动到底,而是把当前可启动的一批一起跑。

这对大型 Web Client 很重要:

  • 不必要的依赖不要人为串行;
  • 启动时间更可控;
  • 依赖图更清晰。

3)错误暴露更可诊断

如果依赖缺失,源码不会给你一个模糊的“undefined is not a function”,而是直接告诉你:

  • 哪些 service 没启动起来;
  • 缺了哪些依赖。

这对定制开发非常友好,因为问题会落在“注册/依赖声明”层,而不是拖到 UI 点击时才炸。

五、为什么 Odoo 还监听 services registry 的 UPDATE 事件

这是一处很容易被忽略、但非常体现框架味道的设计。

startServices 不是只在初始化时扫一遍 registry 就算了,它还给 service registry 挂了 UPDATE 监听。

意味着:

  • 如果后面又有新 service 注册进来;
  • 框架会等待同步代码都跑完;
  • 然后再尝试把新 service 接入已有 env。

这说明 Odoo 考虑的不是“一次性静态启动”,而是运行时仍可能出现扩展注入

对开发者来说,这代表一个重要边界:

你要扩展 Web Client,优先思考“往 registry 里注册什么”,而不是“去改谁的构造函数”。

因为 registry + service 模型本来就是给“后挂载扩展”准备的。

六、env 不是万能容器,但它确实是前端运行时的公共地基

makeEnv() 返回的 env 里有:

  • bus
  • services
  • debug
  • isReady
  • 以及通过 UI service 才能真正工作的 isSmall

这说明 env 的角色不是“什么都能塞的上下文袋子”,而是:

  • 给组件和系统代码一个统一访问入口;
  • 在 service 启动完成前后明确区分状态;
  • 让运行时能力通过 env.services.xxx 对外暴露。

这里最值得注意的是 isReady。它本质上在提醒你:

env 创建出来,不等于 Web Client 已经完全可用;service 全部加载完,才是真正 ready。

这能解释很多自定义里的前端问题:

  • 组件 setup 时直接假设服务可用;
  • 在错误时机访问 env.services
  • 没等 UI service,就读取布局相关能力。

这些都不是“组件写得不够优雅”,而是把运行时阶段搞混了

七、这套设计解决的是“可扩展系统”问题,不只是“代码组织”问题

很多前端框架教程喜欢把 architecture 讲成“分文件、分模块、分目录”。

但 Odoo 这套 registry + service,真正解决的是更难的问题:

1)多个 addon 如何安全共存

一个 ERP 前端不是一两个页面,而是几十上百个模块同时往里加东西。

没有 registry,你只能:

  • 直接改原对象;
  • monkey patch;
  • 手工管理加载顺序。

这在 Odoo 这种生态里会很快失控。

2)可扩展能力如何有统一入口

今天是 field widget,明天是 systray,后天是 public interaction。

如果每种扩展都要单独发明注册机制,开发体验会很差。registry 恰好提供了统一模型。

3)运行时能力如何稳定组装

service 解决的不是“把函数放哪”,而是:

  • 生命周期;
  • 依赖图;
  • 异步初始化;
  • 统一注入。

这就是为什么 Odoo Web Client 虽然看起来是组件化前端,骨架却更像一台插件化操作系统。

八、定制开发里最常见的几个误区

误区 1:为了省事,直接 import 某个内部模块并硬调

这样短期能跑,长期却容易在升级时断裂。

因为你绕开了 registry / service 的公开扩展面,等于把自己绑到内部实现细节上。

误区 2:把 service 当成普通 helper

如果一个能力会被全局多个地方复用,并且依赖别的运行时能力,它更像 service,而不是 utils。

判断标准可以很简单:

  • 是否需要依赖别的服务;
  • 是否需要统一生命周期;
  • 是否需要通过 env 被多处消费。

满足这些特征,就别写成散落的工具函数堆。

误区 3:注册表冲突时只会 force 覆盖

force: true 不是默认扩展姿势。

如果一个 key 冲突就直接覆盖,往往说明你没先想清楚:

  • 是不是应该用新 key 并靠 sequence 排序;
  • 是不是应该通过 patch / hook / subclass 扩展;
  • 是不是在替换本不该替换的核心能力。

误区 4:以为“能 import 到”就代表“扩展姿势正确”

能 import 只是说明路径可见,不说明它是稳定边界。

真正更稳定的边界通常是:

  • registry category
  • service contract
  • component props / hooks
  • 明确暴露的 public API

九、实战里应该怎么用这套思路

如果你要做 Odoo 前端扩展,可以优先按这个顺序判断:

  1. 这是注册型扩展吗? - field、view、systray、interaction、command、effect……先看 registry
  2. 这是运行时公共能力吗? - 涉及全局状态、RPC、通知、布局、弹窗……优先考虑 service
  3. 这是单组件内部逻辑吗? - 再考虑普通组件组合或 hook
  4. 只有在没有公开入口时,才评估 patch 或直接改内部实现

这能显著降低升级时的前端脆弱度。

十、结论

Odoo 前端的难点,从来不只是 OWL 组件怎么写。

更关键的是:一个长期演化、模块众多、允许后装扩展的 Web Client,怎么维持稳定扩展边界。

registry.jsenv.js 可以看出,官方的答案并不是“大家少 import 一点”,而是:

  • registry 管扩展入口、顺序和类型约束;
  • service 管运行时能力、依赖关系和启动过程;
  • env 把这套运行时能力统一暴露给前端系统。

所以真正值得记住的一句话是:

Odoo 前端不是一堆组件拼起来的页面集合,而是一套靠 registry 和 service 组装起来的可扩展运行时。

理解了这一点,你再看 Web Client 的 field、search、dialog、website interaction、editor 插件,很多设计都会突然顺起来。

DISCUSSION

评论区

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