很多人第一次看 Odoo Web 的启动过程,会下意识用普通前端项目的思路去理解:
- HTML 打开;
- 主包下载;
main.js执行;- 页面起来。
但只要顺着 addons/web/static/src/module_loader.js、core/assets.js、env.js、start.js、main.js 往下读,就会发现 Odoo 解决的不是“入口文件怎么跑”,而是另一类问题:
一个可扩展、可拆包、可延迟加载的 ERP 前端,怎么在启动时既不失控,也不把所有东西一次性灌进浏览器。
这就是为什么 Odoo 前端启动链路看起来比普通 SPA 更“分层”。
一、module_loader.js 先解决的不是渲染,而是模块依赖秩序
module_loader.js 最核心的类叫 ModuleLoader。
它内部维护了几组关键状态:
factories:模块工厂函数modules:已经启动完成的模块jobs:等待启动的任务failed:启动失败的模块bus:模块启动事件总线
这说明 Odoo 的第一层关注点不是“把代码打到页面上”,而是:
- 模块有没有定义;
- 依赖是否满足;
- 是否形成循环依赖;
- 启动失败时怎么显式报错。
也就是说,Odoo 在浏览器里自己维持了一层模块运行秩序。
为什么这一步重要
因为 Odoo 前端不是一个小站点,而是很多 addon 共同拼装的系统。
如果只靠“谁先加载谁先执行”,很快就会遇到:
- 某个模块明明存在,却不在正确 bundle 里;
- 某个模块依赖还没好就先执行;
- 某些升级后新增依赖形成环;
- 最后用户只看到一片空白页面。
ModuleLoader.findErrors() 专门检查:
missingfailedcycleunloaded
这不是锦上添花,而是大型插件化前端的保命机制。
二、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 描述;- 区分
cssLibs和jsLibs; - 把已加载资源记进全局缓存与按文档缓存;
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.js 和 start.js:
makeEnv()创建事件总线、service 容器、isReadyPromise;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、编辑器、懒组件这些场景会越来越乱。
而现在的设计把问题拆成了:
- 模块有没有正确声明与排序 ——
module_loader.js - 资源有没有被可靠注入 ——
assets.js - 服务有没有按依赖启动 ——
env.js - 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
评论区