前端

Odoo 为什么敢把网站编辑器拆成“点开再加载”:LazyComponent、loadBundle 与 iframe 资产注入链路讲透

很多人知道 Odoo 能按需加载网站编辑器,却说不清它为什么不会把主文档、iframe 与编辑器资源搅成一团。本文结合 `assets.js` 与 `website_builder_action.js` 源码,讲清 LazyComponent、bundle 描述接口、重试缓存与 targetDoc 注入怎样协同工作。

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

很多人第一次看到 Odoo 网站编辑器,会觉得它像一个“后面又挂了一层前端”。

平时浏览网站时,页面很轻;一旦进入编辑模式,工具栏、拖拽能力、片段面板、弹窗和一堆编辑逻辑才突然出现。直觉上,这种体验背后往往意味着两件事:

  • 资源不能一开始就全量塞进主页面;
  • 编辑器运行环境又不能和普通前台页面完全混在一起。

Odoo 在这里的做法,不是简单的“动态 import 一下”,而是一整套运行时资产装配方案。顺着 addons/web/static/src/core/assets.jsaddons/website/static/src/client_actions/website_preview/website_builder_action.js 往下看,会发现官方真正解决的是这个问题:

当一个重量级前端能力只在少数时刻需要时,怎么把它延迟到真正需要的那一刻,再精确地注入到正确的文档里。

这就是 LazyComponentloadBundle()targetDoc 选项存在的原因。

一、LazyComponent 解决的不是“懒”,而是“先确认资源,再实例化组件”

assets.js 里的 LazyComponent 很短,但意义很大。

它在 onWillStart 里先做两件事:

  1. await loadBundle(this.props.bundle)
  2. 再从 registry.category("lazy_components") 里取真正的组件。

这个顺序非常关键。

很多框架里所谓懒加载,只是把组件代码晚一点下载。但 Odoo 这里把“组件出现”定义成一个更严格的契约:

  • 不是代码文件拿到了就算结束;
  • 而是相关 JS、CSS 乃至目标文档里的资源状态都到位后,组件才允许实例化。

所以 LazyComponent 的价值不是语法糖,而是把资源准备完成变成了渲染前提。

二、/web/bundle/<bundleName> 返回的不是脚本本身,而是资源清单

getBundle(bundleName) 不是直接下载一坨打包产物,而是去请求 /web/bundle/<bundleName>,再把结果拆成:

  • cssLibs
  • jsLibs

这说明 Odoo 的 bundle 在运行时更像“描述文件”而不是“单个大包”。

它带来的收益很实际:

  • 前端能明确知道这次要补哪些 CSS、哪些 JS;
  • 是否注入样式、是否只补脚本,可以在 loadBundle() 参数里控制;
  • 不同文档可以按同一份描述,装配各自需要的资源。

对网站编辑器来说,这尤其重要。因为编辑能力常常一部分属于主后台壳,一部分要真正落到预览 iframe 里。如果 bundle 只是个粗粒度黑盒,就很难做这种细分注入。

三、真正高明的地方在 targetDoc:资源可以进 iframe,而不只进主文档

loadBundle(bundleName, { targetDoc = document, css = true, js = true }) 是整套设计里最值得前端开发者记住的接口。

它意味着 Odoo 不是默认“所有脚本样式都进当前窗口”,而是承认一个复杂事实:

同一套前端能力,可能需要注入到另一个 document。

网站编辑器就是典型场景。

用户看到的是:

  • 外面一层后台/预览壳;
  • 里面一个网站页面 iframe;
  • 编辑行为又要作用在 iframe 内的 DOM。

这时如果你把编辑器 CSS 和 JS 全塞到主文档:

  • iframe 内看不到样式;
  • 某些脚本找不到目标节点;
  • DOM 测量与事件边界会混乱。

targetDoc 让 Odoo 可以把资源精确打到 iframe 的 document.head,让“编辑器运行在 iframe 里”这件事变得可操作。

四、缓存不是一层,而是“全局 + 每个文档”双层缓存

assets.js 里同时有:

  • globalBundleCache
  • assetCacheByDocument

很多人只要看到缓存就会说“防止重复加载”,但这里其实分了两类问题。

1)bundle 级缓存

同一个 bundle 的描述信息,不必每次重复请求 /web/bundle/...

2)文档级资产缓存

同一个 JS/CSS URL 对某个 targetDoc 来说,一旦已经注入,就不要重复再打一次。

为什么一定要按 document 分开?

因为“主文档加载过”并不代表“iframe 文档也加载过”。

这就是网站编辑场景最容易踩坑的地方。很多自研系统懒加载做得半套,结果出现:

  • 主页面缓存命中,但 iframe 其实没资源;
  • 或者相反,错误地认为两个文档是一回事,最终导致编辑区行为不完整。

Odoo 这里用 WeakMap(targetDoc -> Map(url -> Promise)),本质上是在承认:资源加载状态属于具体文档,而不是只属于浏览器 tab。

五、computeBundleCacheMap() 很工程化:先看现实页面里已经有什么

whenReady(() => computeBundleCacheMap(document)) 这行很容易被忽略。

它会扫描 document.head 里现成的:

  • script[src]
  • link[rel=stylesheet][href]

然后把这些 URL 直接标记进缓存。

这说明 Odoo 的资产系统并不天真。它没有假设“只有我自己插入过的资源才算已加载”,而是会先观察页面现实状态。

这在以下场景都很重要:

  • 服务端模板已经先插入部分资源;
  • 页面因为别的引导流程预热过某些文件;
  • 页面恢复或嵌入时,head 里已有现成脚本样式。

这一步让运行时资产系统更像协调器,而不是单纯的 appendChild(script) 工具函数。

六、失败重试是网站编辑器可用性的保底,不是锦上添花

loadCSS()loadJS() 都有重试逻辑,默认带:

  • 重试次数;
  • 固定延迟;
  • 递增额外延迟。

为什么这里值得单独讲?因为懒加载最怕的不是“慢”,而是“点编辑时第一次失败”。

首屏失败用户可能刷新;编辑器失败,用户会直接觉得功能坏了。

对重量级 bundle 来说,临时网络抖动、资源服务器慢、浏览器抢占都可能导致偶发失败。Odoo 通过 Promise 缓存与失败后删缓存再重试,让“晚加载”不至于变成“脆弱加载”。

七、Website Builder Client Action 是这套机制的实际落点

website_builder_action.xml 里可以看到 LazyComponent 被直接用于网站编辑器组件;在 website_builder_action.js 里还能看到编辑模式会主动调用 loadAssetsEditBundle()

这说明网站编辑器不是在初始 WebClient 启动时就绑死进来,而是在:

  • 用户真的进入编辑场景;
  • 状态判定需要编辑能力;
  • 再补对应 bundle。

从架构角度看,这样做有三个明确收益:

1)普通访客首屏更轻

不是每个浏览网站的人都要背上编辑器成本。

2)编辑能力和展示能力边界更清楚

普通前台页面维持轻量;编辑态再追加复杂交互。

3)iframe 隔离更容易维持

因为 bundle 注入天然支持目标文档,编辑区与外层控制壳不会强行混写。

八、对二开最重要的启发:不要只想着“把 JS 文件引进来”

很多 Odoo 前端二开遇到网站编辑器问题时,思路还是:

  • 组件注册了没;
  • 模块 import 了没;
  • 资源放进 manifest 了没。

这些当然重要,但还不够。

更关键的是先问四个问题:

  1. 这份能力属于主文档还是 iframe 文档?
  2. 它是首屏必须,还是进入某模式后再加载?
  3. 对应 bundle 的 CSS/JS 是否要分开控制?
  4. 失败时有没有可靠的重试与缓存语义?

如果这四个问题没想清楚,最常见的后果就是:

  • 主界面有按钮,但 iframe 内样式没进;
  • 代码在后台能 import,实际编辑区行为却不生效;
  • 某些资源偶发加载失败,刷新后又“神奇恢复”。

这类 bug 往往不是业务逻辑错,而是资产注入边界错了

九、一句话总结这套设计的核心价值

Odoo 的 LazyComponent + loadBundle + targetDoc 方案,本质上把网站编辑器这类重能力拆成了三步:

  • 先声明能力属于哪个 bundle;
  • 需要时再加载;
  • 并且加载到正确的 document。

这比“所有东西首屏打平”复杂一些,但对 ERP + CMS 这种混合系统非常值。

因为它解决的不是简单性能优化,而是:

如何在一个同时拥有后台壳、前台页面和 iframe 编辑区的系统里,让复杂前端能力既晚出现、又准出现。

这正是 Odoo 网站编辑体验能兼顾轻量首屏与重度编辑的底层原因。

DISCUSSION

评论区

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