很多人第一次接触 Odoo 视图,会自然地把它理解成这样:
- 后端把 XML arch 发给前端;
- 前端按 XML 结构生成表单或列表;
- 结束。
但如果你顺着 addons/web/static/src/views/view_service.js、view.js、form_arch_parser.js、view_compiler.js 一路看,会发现 Odoo 根本不是“直接画 XML”。
它走的是一条很典型的编译型 UI 管线:
- 加载视图描述;
- 解析 arch,抽出字段与组件语义;
- 编译成 OWL 可运行模板;
- 再交给具体 Controller / Renderer 去跑。
这条链路很值得讲透,因为它解释了很多初学者常见困惑:
- 为什么同一段 XML 不是“有标签就直接显示”;
- 为什么字段节点要先过 parser;
- 为什么 invisible、widget、button 这些东西最后会变成组件逻辑而不是原始 XML 标签;
- 为什么视图二开要同时理解后端元数据和前端编译阶段。
一、view service 先拿到的不是“现成页面”,而是视图描述包
view_service.js 最核心的接口是:
loadViews(params, options)
它最终调用:
orm.cache({ type: "disk" }).call(resModel, "get_views", [], { ... })
注意这里后端方法叫 get_views,不是“render_view”。这已经很能说明问题。
前端拿回来的结果至少包括:
fieldsrelatedModelsviews[viewType].archviews[viewType].id- 可选的
toolbar - 可选的
filters - 可选的
custom_view_id
也就是说,前端收到的是一份视图描述数据包,而不是最终界面。
为什么这里要先把 context 过滤一遍
源码里有一段很重要:只把 lang 和 *_view_ref 之类的键保留下来作为 filteredContext。
这说明官方不想让一大堆运行时上下文直接污染 get_views 结果缓存。
因为视图结构应该尽量由:
- 语言;
- 指定视图引用;
- 设备特征(如 mobile);
- debug 状态;
这些真正影响结构的因素决定,而不是被当前业务动作里各种临时上下文带偏。
这也是为什么 view service 会在 ir.ui.view / ir.filters 更新时触发 CLEAR-CACHES:它把视图描述当成一类值得缓存、也必须在结构变更后主动失效的数据。
二、View 组件做的第一件事不是 render,而是 loadView
view.js 里 View 组件在 onWillStart 阶段先执行:
this.loadView(this.props)
这一步特别关键。
因为它说明 View 不是一个“拿到 arch 立刻画出来”的 dumb component,而是一个带加载期和运行期的容器。
它先检查:
resModel是否存在;type是否有效;arch/fields是否成对给出;searchViewArch/searchViewFields是否成对给出。
这些校验都在提醒你:
一份可运行视图,不是单独一段 arch 就够了,还必须配套字段定义、搜索视图、配置与上下文。
三、ArchParser 不是“读 XML”,而是在提炼运行时语义
以 form_arch_parser.js 为例,它做的绝不只是遍历节点。
它会在 visitXML 过程中:
- 对
<field>调Field.parseFieldNode(...) - 对
<widget>调Widget.parseWidgetNode(...) - 给每个字段节点生成
field_id - 收集
fieldNodes/widgetNodes - 记录
autofocusFieldIds - 判断
disable_autofocus - 计算
activeActions
这说明 parser 的目标不是“保留 XML 长相”,而是把 XML 里那些对运行时有意义的信息提前抽出来。
举个很直观的点:
同名字段在一个 form 里可以出现多次,所以 parser 不能只记字段名,必须给每个出现位置生成唯一 field_id。
如果没有这一步,后面编译和渲染阶段就分不清“这个 partner_id 是页头那个,还是 notebook 页里的那个”。
parser 其实在做“从声明式结构到可执行描述”的第一轮翻译
XML arch 写的是声明式视图结构;
parser 产出的则更接近:
- 哪些字段节点存在;
- 它们各自的配置是什么;
- 哪些 widget 节点存在;
- 用户动作能力有哪些;
- 焦点与行为元信息如何组织。
所以 parser 是视图编译链里第一层真正的“语义提纯器”。
四、ViewCompiler 才是把 arch 送进 OWL 世界的关键桥梁
view_compiler.js 更能看出 Odoo 的思路。
ViewCompiler 默认就会对这些节点做专门处理:
a[type]/buttonfieldwidget
也就是说,在 Odoo 眼里:
- button 不是普通 HTML button;
- field 不是普通 XML tag;
- widget 也不是最终 DOM。
它们都是需要被编译成组件或行为节点的声明式语法。
compileNode 不只是复制节点,而会处理 modifiers 与 Owl 指令
源码里能看到几件很典型的事:
- 会去掉
t-translation - 会读取 modifier,比如
invisible - 会把 invisible 编译成
t-if - 会识别
t-att-*、t-attf-*等 Owl 指令 - 会根据目标是不是组件节点,决定 attribute 如何搬运
这说明 Odoo 的视图编译,不是把 XML“原样变 HTML”,而是把声明式语法翻译成 OWL 模板表达式 + 组件 props + 条件渲染逻辑。
尤其 applyInvisible() 很说明问题:
- 它不会简单给节点加个 CSS class 隐藏;
- 而是把表达式编译进
t-if; - 运行时再基于 record 的
evalContextWithVirtualIds求值。
这比“渲染完再隐藏”更靠近真正的视图语义。
五、为什么 field、button、widget 必须走编译,而不是让浏览器自己认标签
因为这些标签根本不是浏览器语义,而是 Odoo 自己定义的声明式 UI 语言。
比如 <field> 至少隐含了这些问题:
- 用哪个字段组件;
- 只读还是可编辑;
- 用哪个 formatter / parser;
- 有哪些 modifiers;
- 与 record / model 的绑定怎么建立。
浏览器当然不懂这些。
所以 Odoo 必须先把它们翻译成前端运行时能执行的组件树。也正因为如此,视图不是“模板片段”,而更像一段 DSL(领域专用语言)。
六、加载、解析、编译分层,正是为了让视图系统可扩展
很多人会觉得这条链很长,似乎复杂过头。但如果你把 Odoo 的使用场景放进来,就知道这其实是必要复杂度。
1)后端可以主导声明式结构
业务模块通过 XML 声明视图结构,而不用自己写整套前端组件。
2)前端可以在 parser / compiler 阶段接管运行时语义
这让同一份 arch 不只是“展示结构”,还能表达:
- 字段行为;
- 条件显示;
- 组件挂载;
- 按钮动作;
- 搜索 / toolbar / 自定义视图元数据。
3)不同 view type 可以替换各自 parser / compiler
列表、表单、kanban、calendar 都有自己的 arch parser,说明官方不是一把梭地处理所有视图,而是允许每种视图在共享总框架下定制自己的编译逻辑。
七、实战里最常见的几个误区
误区 1:把 arch 当最终界面
arch 只是输入,不是结果。真正结果是 parser 和 compiler 消化后的运行时组件结构。
误区 2:前端二开只盯 XML,不看 parser / compiler
这样最容易出现“标签写了但行为不对”“字段明明在 arch 里却没按预期工作”的困惑。
误区 3:把 invisible 当成纯 CSS 隐藏
在 Odoo 里,modifier 更像视图语义的一部分,很多时候是在编译和表达式求值层解决,不只是样式层。
误区 4:忽视 view 缓存边界
get_views 结果会缓存,结构变化也会触发清缓存。如果不了解这一点,很容易把“前端没刷新”误判成“源码没生效”。
八、结论
Odoo 视图系统的关键,不是“后端返回 XML,前端照着画”,而是:
get_views返回结构化视图描述;- ArchParser 把声明式节点提炼成运行时语义;
- ViewCompiler 把这些语义翻译成 OWL 模板和组件逻辑;
- 最终由具体 View / Controller / Renderer 组合成真正可交互界面。
所以更准确的一句话是:
Odoo 视图不是 XML 驱动的静态页面,而是一条把声明式视图语言编译成前端运行时的管线。
理解了这条管线,你再去改字段 widget、按钮行为、modifiers、搜索视图或自定义 view type,思路会清楚很多:你不是在“改一段 XML”,而是在接入一条编译链。
DISCUSSION
评论区