前端

Odoo 字段组件为什么不是“拿到 value 渲染一下就完了”:field registry、props、parse/format 与输入提交链路讲透

很多人做 Odoo 字段二开时,会把 field widget 理解成“组件接个值、改一下模板”。但从 field.js、standard_field_props.js、input_field_hook.js、formatters.js、parsers.js 到具体字段组件的链路看,官方真正维护的是一整套值流系统:字段解析、组件选型、显示格式、输入脏态、提交时机与记录更新。本文结合源码,讲清 Odoo 字段为什么很少是“纯展示组件”。

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

很多人第一次二开 Odoo 字段组件,最容易低估的一件事就是:

字段组件看上去像普通前端组件,实际上它更像“记录系统的前端端口”。

你如果只把它理解成:

  • 传进来一个 value;
  • 显示一下;
  • 用户改完再回传;

很快就会在真实业务里踩到各种怪问题:

  • 为什么有的字段明明改了,模型没更新;
  • 为什么 onchange 一回来,输入中的值没被覆盖;
  • 为什么有的字段显示值和原始值不是一回事;
  • 为什么同一个 widget 在 list / form / 不同 jsClass 下还能切不同实现。

只要顺着 field.jsstandard_field_props.jsinput_field_hook.jsformatters.jsparsers.js 和具体字段组件例如 char_field.js 看下去,就会发现 Odoo 维护的根本不是“组件接值”,而是一条完整的字段值流

一、字段组件的入口不是组件本身,而是 field.js 的注册与选型

field.js 里最关键的不是模板,而是 getFieldFromRegistry()Field.parseFieldNode()

这两段代码说明:

  • 字段最终渲染哪个组件,不是写死的;
  • 它会根据 fieldTypewidgetviewTypejsClass 去 registry 里找;
  • 找不到 widget 时还会回退;
  • 不支持当前字段类型时会显式警告。

这说明 Odoo 的字段体系首先是注册驱动而不是组件直连

也就是说,字段 UI 从一开始就不是“某字段永远绑定某组件”,而是:

当前视图语境下,这个字段该用哪种实现。

这也是为什么 Odoo 字段二开,很多时候应该先想 registry 入口,而不是直接去改某个组件模板。

二、standardFieldProps 暗示字段组件并不是自由发挥的普通组件

standard_field_props.js 看起来很简单,但意义很大。

它至少明确了几件事:

  • name
  • record
  • readonly
  • id

这些不是随意传参,而是字段组件的最小契约。

其中最关键的是 record

因为这说明字段组件面对的不是单个裸值,而是一整个记录上下文。于是字段组件天然可以接触:

  • record.data
  • record.fields
  • 字段元信息
  • 当前编辑状态
  • 验证状态
  • 更新方法

所以字段组件本质上不是“value component”,更像是record-bound component

三、显示值与存储值本来就不是一回事,formatters.js 只是把这个事实写明了

很多初学者会默认认为:

  • 模型里存什么;
  • 页面上就显示什么。

Odoo 明确不是这么做的。

formatters.js 提供了大量格式化入口,例如:

  • formatChar
  • formatFloat
  • formatDate
  • formatDateTime
  • formatBoolean
  • formatBinary

这说明字段显示层真正关心的是:

用户现在应该看到什么文本 / 标记,而不是数据库里原始值长什么样。

例如:

  • 密码字段可以显示成星号;
  • 浮点数要考虑本地化分隔符与精度;
  • 日期显示分 numeric / locale 风格;
  • 布尔值甚至可以格式成一段标记化 HTML。

所以 field widget 二开时,如果只盯着 record.data[name],往往只看到了值流的一半。

四、输入时也不是“边敲边写模型”,useInputField 管的是脏态与提交边界

input_field_hook.js 是字段体系里非常值得反复读的文件。

它解决的核心问题是:

用户正在编辑时,界面输入框和底层 record 更新并不总是同一步。

源码里能看到几个关键状态:

  • isDirty
  • lastSetValue
  • pendingUpdate

这三个变量一下就把问题讲清楚了。

1)为什么需要 dirty

因为用户在输入框里改的内容,可能还没真正提交给 record。

2)为什么要记 lastSetValue

因为组件必须区分:

  • 当前输入框内容是用户正在打字形成的;
  • 还是最近一次已提交给模型的稳定值。

3)为什么要有 pendingUpdate

因为某次 record.update() 可能还在路上,比如 onchange 未返回。这时如果发生紧急保存,系统得知道有哪些改动尚未被确认。

这说明 Odoo 字段不是简单的“双向绑定”,而是一个前端输入态与模型态之间的同步协议

五、onInputonChangecommitChanges 三层时机,说明提交不是单一事件

useInputField 至少分了三层动作:

  • onInput:标记 dirty、处理局部校验状态
  • onChange:blur / change 时尝试 parse 并提交
  • commitChanges:在更关键时机强制同步,例如 urgent save

这个拆分特别符合业务表单的现实。

因为真实字段输入不只是“失焦提交”这么简单,还会遇到:

  • Tab / Enter 导航
  • 紧急保存
  • 模型请求本地变更
  • onchange 异步回写

如果没有 commitChanges 这层,很多“用户明明刚打完字,怎么保存没带上”的问题就会不断出现。

六、parsers.js 说明“用户输入的字符串”也不是最终值

和 formatter 对应,parsers.js 负责把用户输入变成模型能接的值。

这里最容易被忽略的一点是:Odoo parser 不是只做基础类型转换,它还吸收了大量业务型输入习惯。

比如:

  • 按本地化千分位 / 小数点解析数字;
  • 支持 =1+2 这种数学表达式;
  • 对整数边界做约束;
  • 对百分比、金额、时间格式做专门处理;
  • 某些场景还能解析运算型输入如 += 3

这就说明字段系统的职责不只是“把值写回去”,还包括:

把用户语言翻译成模型语言。

这也是为什么 parser 失败时,不是简单忽略,而是会把字段标记成 invalid。

七、char_field.js 暗示具体字段组件其实是在“拼接公共值流能力”

char_field.js 很有代表性。

它并没有把所有逻辑自己重写,而是组合:

  • standardFieldProps
  • useInputField
  • formatChar
  • 自己的 parse() 规则
  • 以及字段元信息里的 trimsizetranslate

这说明具体字段组件的职责更像:

  1. 声明自己支持什么字段类型;
  2. 决定显示和解析的细节;
  3. 把公共输入 / 提交流程接上。

官方不是把每个字段都写成一套孤立世界,而是在同一条值流框架上做变体。

八、为什么 onchange 回来时不会随便覆盖用户输入

useInputField 里有一段非常关键的逻辑:

  • 如果当前 isDirty
  • 或字段仍是 invalid;
  • patch 时就不要随便把输入框值改掉。

这个细节特别体现 Odoo 对真实表单体验的理解。

因为在业务系统里,模型更新可能来自:

  • onchange 返回;
  • 其他字段联动;
  • 服务端修正;
  • 外部刷新。

如果每次 record 有更新就强行覆盖输入框,用户会感觉自己刚打的内容“被系统吃掉了”。

所以 Odoo 在这里维护的是一个很重要的原则:

模型可以更新,但不能粗暴夺走用户当前编辑权。

九、字段二开里最常见的几个误区

误区 1:把字段组件当纯展示组件

结果一到编辑态、校验态、readonly 切换就开始出问题。

误区 2:绕过 parser / formatter,直接字符串进字符串出

短期看能跑,长期一定会在本地化、精度、金额、日期这些问题上翻车。

误区 3:自己监听 input 然后直接 record.update

如果不接入现成 hook,就很容易把 dirty、pending update、urgent save 边界全弄丢。

误区 4:忽略 registry 选型层

有些问题其实不该在组件内部硬改,而应该从 widget 注册和适用范围上解决。

十、结论

Odoo 的字段组件从来不是“拿个值渲染一下”的轻量视图。

从源码链路看得很清楚:

  • field.js 负责字段节点解析与 widget 选型;
  • standardFieldProps 定义字段组件契约;
  • formatters.js 决定显示层长什么样;
  • parsers.js 把用户输入翻译成模型值;
  • useInputField 维护 dirty、提交与同步边界;
  • 具体字段组件在这套公共值流之上再补自己的规则。

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

Odoo field widget 不是一个接 value 的小组件,而是 record、显示、输入、校验与提交之间的前端值流节点。

理解了这一点,你再做字段二开,很多“明明只是改个 input,怎么越改越乱”的问题,就能更早避开。

DISCUSSION

评论区

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