前端

Odoo 前端启动为什么不是“页面一开就全量跑完”:asset bundle、module loader 与 WebClient 引导链路讲透

很多人把 Odoo 前端启动理解成“浏览器把 JS 下载完就开始跑”。但从 module_loader.js、assets.js、env.js、start.js 到 main.js 的链路看,官方真正做的是一套分层引导系统:先解析模块依赖,再按需加载 bundle,最后把服务和 WebClient 挂起来。本文结合官方源码,讲清 Odoo Web 为什么能在大型插件化前端里避免“首屏全塞满”。

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

很多人第一次看 Odoo Web 的启动过程,会下意识用普通前端项目的思路去理解:

  • HTML 打开;
  • 主包下载;
  • main.js 执行;
  • 页面起来。

但只要顺着 addons/web/static/src/module_loader.jscore/assets.jsenv.jsstart.jsmain.js 往下读,就会发现 Odoo 解决的不是“入口文件怎么跑”,而是另一类问题:

一个可扩展、可拆包、可延迟加载的 ERP 前端,怎么在启动时既不失控,也不把所有东西一次性灌进浏览器。

这就是为什么 Odoo 前端启动链路看起来比普通 SPA 更“分层”。

一、module_loader.js 先解决的不是渲染,而是模块依赖秩序

module_loader.js 最核心的类叫 ModuleLoader

它内部维护了几组关键状态:

  • factories:模块工厂函数
  • modules:已经启动完成的模块
  • jobs:等待启动的任务
  • failed:启动失败的模块
  • bus:模块启动事件总线

这说明 Odoo 的第一层关注点不是“把代码打到页面上”,而是:

  1. 模块有没有定义;
  2. 依赖是否满足;
  3. 是否形成循环依赖;
  4. 启动失败时怎么显式报错。

也就是说,Odoo 在浏览器里自己维持了一层模块运行秩序

为什么这一步重要

因为 Odoo 前端不是一个小站点,而是很多 addon 共同拼装的系统。

如果只靠“谁先加载谁先执行”,很快就会遇到:

  • 某个模块明明存在,却不在正确 bundle 里;
  • 某个模块依赖还没好就先执行;
  • 某些升级后新增依赖形成环;
  • 最后用户只看到一片空白页面。

ModuleLoader.findErrors() 专门检查:

  • missing
  • failed
  • cycle
  • unloaded

这不是锦上添花,而是大型插件化前端的保命机制。

二、Odoo 的模块加载不是“定义完马上跑”,而是等依赖满足再启动

define(name, deps, factory, lazy = false) 的设计很关键。

模块定义后并不会盲目立刻执行,而是先进入 factories,再通过 addJob() 放进待启动队列。随后 startModules() 会不断找出“依赖已经存在”的 job 执行。

这个机制的含义是:

Odoo 前端启动顺序,本质上由依赖图决定,而不是由 import 文件顺序决定。

这有三个直接收益:

1)启动顺序更稳

不会因为打包顺序偶然变化就把系统搞崩。

2)错误更可诊断

如果依赖缺失,控制台会明确提示“可能不在正确 asset bundle 中”,而不是只给一个模糊的 undefined

3)支持懒模块

lazy 参数意味着某些模块可以只定义不立即进入启动流程,给后续按需加载留下空间。

三、assets.js 解决的是“资源什么时候真正进文档”

很多人只盯着 JS 模块,却忽略了 Odoo 还有一层资源装配问题。

core/assets.js 做了几件非常工程化的事:

  • getBundle(bundleName) 通过 /web/bundle/<bundleName> 拉取 bundle 描述;
  • 区分 cssLibsjsLibs
  • 把已加载资源记进全局缓存与按文档缓存;
  • loadCSS / loadJS 支持重试;
  • loadBundle 支持把资源打到指定 targetDoc,例如 iframe 文档。

这说明在 Odoo 里,“加载一个功能”不只是 import 一个模块,往往还意味着:

  • 对应 CSS 也要补进来;
  • 资源可能属于外部 bundle;
  • 同一个 bundle 不能重复打入;
  • 某些场景还要加载到 iframe 而不是主文档。

computeBundleCacheMap() 透露了什么

它会扫描当前 document.head 里已经存在的 script[src]link[rel=stylesheet],提前把这些 URL 视作已加载。

这个细节很有意思,它说明 Odoo 不是单纯相信“我自己刚刚加载过什么”,而是主动和页面现实状态对齐。

所以 asset 系统更像运行时资源协调器,而不是一个简单工具函数。

四、LazyComponent 说明 Odoo 把“延迟渲染”做成了官方能力

assets.js 里的 LazyComponent 非常值得注意。

它在 onWillStart 阶段先 await loadBundle(this.props.bundle),然后再从 registry.category("lazy_components") 里取真正组件。

这套设计解决的不是语法懒加载,而是业务级按需装配

  • 功能不用一上来就进首包;
  • bundle、组件注册、渲染三件事被拆开;
  • 扩展模块依旧可以通过 registry 接入。

换句话说,Odoo 的按需加载不是“某个页面偷偷 import() 一下”,而是把 bundle 与组件可见性一起纳入统一机制。

五、env.js 才是从“代码已到位”切到“运行时可用了”的分水岭

main.js 其实很薄,只是调用 startWebClient(WebClient)

真正重要的事情在 env.jsstart.js

  • makeEnv() 创建事件总线、service 容器、isReady Promise;
  • startServices(env) 按依赖图启动服务;
  • mountComponent() 在 root app 场景下先启动服务,再挂载 OWL 应用。

这意味着:

浏览器拿到 JS,并不等于 Odoo 前端已经 ready;服务启动完成,才算真正进入可用状态。

所以很多二开里“组件能渲染,但功能就是不对”的问题,根子往往不在模板,而在运行时阶段被搞混了。

六、start.js 把 WebClient 启动做成了一个正式阶段,而不是裸挂组件

startWebClient() 里做的事情非常完整:

  • 写入 odoo.info
  • 根据 browser_cache_secret 接上 RPC 缓存
  • whenReady() 等 DOM 就绪
  • mountComponent(Webclient, document.body, ...)
  • 根据语言方向、超级管理员、debug、触屏能力给 body 加类名
  • 最后把 odoo.isReady 设成 true

这说明 Odoo 启动 WebClient 时,不是“把根组件 mount 了就完事”,而是有一整段环境定型流程。

这段流程的价值在于:

  • UI 和运行时状态一起落地;
  • 首次挂载与后续服务消费有清晰边界;
  • WebClient 拿到的是已经装好的环境,不是半成品。

七、asset bundle 和 module loader 不是两套重复系统,而是上下两层

很多人刚读源码时,会觉得这两层有点重复:

  • module_loader.js 在管模块;
  • assets.js 也在管 JS。

其实它们分别解决的是两类问题:

module_loader.js 管的是模块语义依赖

  • 哪个模块定义了;
  • 依赖模块是否存在;
  • 工厂函数能否安全执行。

assets.js 管的是物理资源进入页面

  • 哪些 CSS / JS 文件需要注入;
  • 是否已经加载;
  • 要打到哪个文档;
  • 失败后是否重试。

前者更像运行时模块编排器,后者更像资源投递器。

两者合起来,Odoo 才能做到:

既知道“代码逻辑上能不能运行”,也知道“资源物理上有没有到位”。

八、这套设计为什么特别适合 Odoo 这种 addon 生态

因为 Odoo 的前端不是一个封闭应用,而是长期被 addon 追加能力的系统。

如果没有这套分层启动:

  • 新功能只能硬塞首包;
  • 扩展模块难以控制依赖;
  • 某个 bundle 放错位置会在深层才炸;
  • iframe、编辑器、懒组件这些场景会越来越乱。

而现在的设计把问题拆成了:

  1. 模块有没有正确声明与排序 —— module_loader.js
  2. 资源有没有被可靠注入 —— assets.js
  3. 服务有没有按依赖启动 —— env.js
  4. WebClient 有没有在完整环境中挂载 —— start.js

这就是典型的“大系统分阶段引导”思路。

九、二开时最容易踩的坑

误区 1:把 asset 问题当模块问题

控制台报模块缺失,不一定是模块源码没写,很多时候是 bundle 根本没把文件带进去。

误区 2:把能 import 到当成已经可用

某段代码 import 成功,不等于对应 service 已启动,也不等于相关 CSS 已经加载。

误区 3:首屏什么都想塞进去

Odoo 已经给了 bundle、lazy component、按需资源加载这套机制,继续无脑首包堆料,只会让 WebClient 越来越重。

误区 4:忽略 iframe / editor 等多文档场景

targetDoc 的存在不是装饰品。某些功能如果只考虑主文档,到了 iframe 内马上出错。

十、结论

Odoo 前端启动链路,真正厉害的地方不在于“入口文件写得整齐”,而在于它把启动拆成了几层清晰责任:

  • module_loader.js 负责模块依赖秩序;
  • assets.js 负责 bundle 与资源注入;
  • env.js 负责服务运行时装配;
  • start.js / main.js 负责把 WebClient 正式拉起。

所以更准确的理解应该是:

Odoo Web 的启动不是一次性执行脚本,而是一条从模块、资源、服务到 UI 的分阶段引导链。

理解了这条链路,你再看懒加载、编辑器、iframe、网站构建器、甚至某些“怎么刷新后才正常”的奇怪问题,都会更容易找到根因。

DISCUSSION

评论区

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