很多人第一次看 Odoo 网站的 dynamic snippet,会把它理解成:
- 前端调个接口拿数据;
- 选个模板渲染出来;
- 结束。
但 addons/website/static/src/builder/plugins/options/dynamic_snippet_option_plugin.js 说明,官方真正关心的并不是“能不能显示数据”,而是:
- snippet 刚拖进页面时如何自动补齐默认配置;
- 过滤器和模板如何缓存;
- 模板切换时 DOM 与 dataset 如何同步;
- 单条模式和多条模式怎么分流;
- 页面编辑态和最终显示态之间如何保持一致。
所以 dynamic snippet 本质上不是“模板 + RPC”,而是一条完整的选项驱动渲染管线。
一、dynamic snippet 真正的核心,不是渲染函数,而是 option plugin
很多人下意识会去找最终模板长什么样,或者接口返回什么 JSON。
但从源码看,真正的中枢在 DynamicSnippetOptionPlugin:
- 它负责注册 builder option
- 提供 builder action
- 处理 snippet dropped 事件
- 拉取 filter 和 template
- 决定默认 dataset
- 更新模板相关 class / dataset / 容器样式
这说明官方把 dynamic snippet 视作“编辑器里的可配置内容块”,而不是单纯的前台小组件。
换句话说,编辑态的配置机制才是第一主角。
二、为什么 snippet 刚拖进页面时,要先进入 loading / 非 empty 状态
onSnippetDropped() 里有个很值得注意的动作:
- 如果匹配 dynamic snippet selector,就先
setOptionsDefaultValues - 如果带
s_dynamic,就移除o_dynamic_snippet_empty - 并添加
o_dynamic_snippet_loading
这一步很能体现 Odoo 的产品思路。
官方不希望用户刚拖一个动态块进来,就看到一个“什么都没有”的死壳子。它要明确区分三种状态:
- empty:还没进入正常动态内容装载语义
- loading:正在等待配置或数据
- ready:配置和渲染完成
这意味着 dynamic snippet 的状态不是只靠数据接口结果决定,而是由编辑器配置流程一起驱动。
三、默认值不是拍脑袋写死,而是通过 filter / template 双缓存决定
插件里建了两个 Cache:
dynamicFiltersCachedynamicFilterTemplatesCache
分别包着:
/website/snippet/options_filters/website/snippet/filter_templates
这说明官方把“可选过滤器”和“可用模板”看成两个不同维度的资源。
为什么要拆两层缓存
因为它们变动频率和使用场景不同:
- filter 更偏业务对象与数据来源
- template 更偏展示结构与样式能力
如果混成一个大响应包,编辑态的切换和复用都会更重。
拆开后可以更灵活:
- 换模型时只重取相关模板
- 同模型下多个 snippet 共享模板缓存
- 多次打开 option 面板不必重复请求
这不是性能小技巧,而是在明确:
dynamic snippet = 数据选择层 + 展示模板层,两者分离。
四、dataset 才是 snippet 配置的真实载体,不是组件 state
setOptionsDefaultValues() 和 updateTemplate() 大量在改 snippetEl.dataset:
snippetModelsnippetResIdtemplateKeyfilterIdnumberOfRecordsnumberOfElementsnumberOfElementsSmallDevicesextraClassescolumnClasses
这背后是一个很重要的前端设计选择:
Odoo 网站 builder 的配置,要落在 DOM 上
为什么不用纯 JS state?因为 snippet 不是普通内存组件,它必须:
- 在编辑器里可见、可复制、可保存
- 写回 HTML 结构
- 在前台渲染时还能继续读出配置
- 被别的插件、模板、交互逻辑继续消费
所以 dataset 在这里不只是“方便取值”,而是持久化配置协议。
这也解释了为什么很多自定义 snippet 二开容易不稳:
- 只在组件内存里改了值
- DOM 上没留下可持久化语义
- 保存后或切换编辑态后状态丢失
五、模板切换不是简单替换 HTML,而是一整套 class / dataset / 容器同步
updateTemplate() 很值得细看。
它做的事情不是“new template render 一下”,而是逐步同步:
- 更新
templateKey - 移除旧 template 对应 class
- 添加新 template 对应 class
- 同步
numberOfElements/numberOfRecords等 dataset - 更新 container 的 class
- 更新 content 的 class
- 更新 snippet 根节点的额外 class
- dispatch
dynamic_snippet_template_updated
这说明官方理解中的“换模板”,不是一块孤立的视图替换,而是:
- 外层容器可能要变
- 栅格列数可能要变
- 获取条数可能要变
- 根节点语义 class 也可能要变
- 别的插件还得知道模板变了
所以 dynamic snippet 模板,本质上更接近一个“布局协议包”,而不是单独的 QWeb 片段。
六、为什么要区分 single mode 和 normal mode
插件里有 isSingleModeSnippet()、getDefaultSnippetRecordId()、snippetResId 等逻辑。
这说明官方明确把两类场景区分开:
1)单记录模式
典型像:
- 指定某一个产品
- 指定某一篇博客
- 指定某一个案例
这时核心配置是:
- 模型名
- 具体记录 ID
- 与单记录匹配的模板
2)多记录模式
典型像:
- 最新文章列表
- 推荐产品轮播
- 某个过滤器下的记录集合
这时核心配置变成:
- filterId
- 拉取条数
- 每屏元素数
- 模板布局参数
如果这两套逻辑混在一起,你会遇到非常多边界混乱:
- 选了单条记录却还保留 list filter
- 模板要求轮播却只有 1 条记录
- 默认值补齐时不知道该优先 record 还是 filter
官方专门拆 single mode,就是为了避免这类“配置维度不一致”的问题。
七、模板 class 不是装饰,而是从 template key 到 DOM 语义的桥
getTemplateClass(templateKey) 会把模板 key 映射成 snippet class,例如转成 s_* 风格类名。
这一步的价值在于:
- HTML 结构本身带出当前模板身份;
- 编辑器、样式层、前台渲染层都能通过 class 感知模板;
- 即使不重新解析一大坨配置,也能快速判断当前 snippet 采用哪套模板语义。
这说明 class 在这里不是纯视觉样式,而是模板语义标识。
八、dispatch 事件说明 dynamic snippet 不是孤立插件
updateTemplate() 最后会 dispatch dynamic_snippet_template_updated。
这代表 Odoo 已经预设了一个事实:
模板切换后,别的插件或逻辑可能也需要跟着做事。
这是一种非常健康的扩展设计。
相比把所有后续逻辑写死在一个插件内部,事件分发让别的模块有机会:
- 响应模板变化
- 调整附属 UI
- 做额外校验
- 做后续重排
这也是 Odoo 前端常见的思路:核心插件负责定义主语义,协同插件靠事件接力。
九、实战里最容易踩的坑
误区 1:只改模板,不改 dataset
这样编辑器里可能暂时看着正常,但保存、刷新或切换选项后状态会乱。
误区 2:把 filter 和 template 当成一个概念
filter 决定“取什么”,template 决定“怎么摆”。两者耦合过死,扩展会非常难受。
误区 3:忽略 loading / empty 状态
一旦异步拉取稍慢,页面就会闪空白、抖布局,用户体验非常差。
误区 4:单记录和多记录共用一套默认值逻辑
这会让 record、filter、count、carousel 参数互相污染。
误区 5:只在编辑器里能跑,前台最终 HTML 却没有保留语义
这种二开短期 demo 很像成功,真正保存发布后就开始崩。
十、结论
Odoo 网站的 dynamic snippet,真正复杂的地方从来不是“调个接口回来渲染一下”。
它更像一条由 option plugin 驱动的前端内容流水线:
- 先识别 snippet 类型
- 再补齐 dataset 默认值
- 拉过滤器与模板资源
- 区分单条 / 多条模式
- 切换模板类名与容器结构
- 用 loading / empty 状态保证体验
- 再通过事件让协同插件接力
所以更准确的理解是:
dynamic snippet 不是一个小组件,而是一套“可编辑、可持久化、可切换模板”的内容配置协议。
理解这点之后,你再去做文章流、产品流、案例流之类的动态块定制,思路就会稳很多:先设计配置协议,再谈渲染细节。
DISCUSSION
评论区