重新发明已解决的问题:Odoo OWL 前端框架的架构评审
Source: Dev.to
Introduction
在本文中,我想聚焦 Odoo 的 OWL 框架——Odoo Web 堆栈中前端复杂性的第一层主要层——并质疑构建它是否真的必要,或者它是否是一个可以避免的长期复杂性来源,仅仅因为常见的论点:“它是 ERP,所以必须复杂。”
为了提供背景,OWL (Odoo Web Library) 是用于驱动 Odoo 前端组件的 JavaScript 框架,包括仪表盘、后台 UI 和网站的部分功能。
根据 Odoo 的说法,OWL 是从零开始构建的,旨在解决一个特定问题:让第三方开发者能够 覆盖并扩展其他模块定义的前端组件,而无需修改核心文件或在升级时丢失更改。
纸面上,这一目标是合理的——甚至值得称赞。然而,得出必须 构建全新前端框架 的结论则更加值得商榷。相同的目标已经可以在所有主流现代前端框架(React、Vue、Angular)中通过成熟的机制实现,例如组件组合、插槽、高阶组件、依赖注入、扩展 API 和基于模式的渲染。
成熟的前端框架本可以为 Odoo 提供的内容
-
清晰、持续演进的文档
成熟的框架拥有持续更新的文档,能够紧密跟踪实际使用情况和功能特性。相比之下,Odoo 的文档常常不完整、过时或误导——这一问题足以单独成文。 -
安全响应性
现代前端生态系统能够快速响应安全披露,发布补丁而无需进行完整的应用升级。而在 Odoo 中,前端修复与后端发布紧密耦合,使得安全补丁的应用变得相当繁琐。 -
庞大的生态系统
从表单构建器、模式验证器到可访问性工具、测试框架以及 UI 组件库——现代生态系统提供的解决方案,要么被 Odoo 仅部分重实现,要么根本缺失。 -
开发者熟悉度
如今的前端开发者已经熟练掌握 React、Vue 或 Angular。OWL 引入了一套专有的思维模型,开发者必须在已经相当复杂的 Odoo 后端抽象之上额外学习。
当前混合技术栈
Odoo 现在维护一个混合前端栈,OWL 与传统代码共存——系统的某些部分仍然包含多个版本的 jQuery(2.x 和 3.x)。仅此一点就应当引发对长期可维护性的质疑。
在直接将 OWL 与 React 或 Vue 进行比较之前,先了解 OWL 的实际工作方式非常重要。
OWL 的工作原理
在核心层面,OWL 消费从后端发送的 XML 定义,并在前端动态构建组件树。这些 XML 视图会被解析、解释,并转化为由 JavaScript 渲染的 UI 组件。
示例:一个简单的 OWL 表单定义
前端接收到该 XML 后,使用 OWL 运行时将其转化为渲染后的 UI 组件:
- 将 XML 解析为组件树。
- 发起额外请求以获取模型字段的元数据。
- 根据字段定义决定渲染哪个组件。
- 可选地使用
widget属性选择自定义渲染器。
换句话说,OWL 充当 运行时 XML 解释器,动态生成 UI 行为。
关于 OWL 的严峻真相
OWL 的灵活性并非源自根本性的全新理念——它来源于将结构和行为决策推迟到运行时。这并不独特,也不需要自定义框架。
例如,在 React 中也可以通过以下方式实现同等水平的灵活性:
- 基于模式的渲染
- 插件注册表
- 声明式扩展点
- 通过依赖注入进行受控的覆盖
- 有权限的组件替换
许多系统已经能够解析 XML、JSON 或 DSL,并在成熟的框架中安全渲染它们——无需重新发明渲染生命周期、状态管理、响应式或工具链。
选择构建 OWL,Odoo 承担了以下负担:
- 维护专有框架
- 重建已有的工具链
- 前端知识碎片化
- 将前端演进耦合到后端架构约束
在后续章节中,我将演示如何使用 React 干净地渲染和覆盖 Odoo 基于 XML 的 UI 模型,实现相同的可扩展性而无需引入自定义框架。目标并不是声称 OWL 不可用——而是展示构建它是一种不必要的架构选择,增加了长期成本,却没有解决任何新问题。
一个简单的 OWL 组件 vs. React 组件
OWL 组件(简化版)
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
export class Hello extends Component {
static template = xml`
Hello
`; }
使用通常与从后端发送的 XML 视图定义绑定,组件的生命周期、状态处理以及渲染行为均由 OWL 的自定义运行时管理。
### React 组件(等价)
```javascript
function Hello({ name }) {
return (
你好 {name}
); }
从表面上看,这两个组件都很简单。关键的区别不在于语法——而在于 **生态系统的重量级**。
在 React 中:
- 组件组合是标准做法。
- 工具链(linting、测试、性能分析)已经成熟。
- 状态、effect 和错误边界都有明确的定义。
- 与基于 schema 的渲染集成已是常规。
OWL 必须重新实现或近似所有这些能力。
## React 伪实现,映射 OWL 重写
一个常见的对 OWL 的辩护是它允许 *运行时 UI 重写* —— 模块能够在不编辑核心代码的情况下动态替换或扩展 UI 行为。
这 **并非** OWL 独有。
下面是一个简化的 **基于 React 的架构**,实现了相同的能力。
### 组件注册表(核心)
```javascript
const ComponentRegistry = new Map();
export function registerComponent(name, component) {
ComponentRegistry.set(name, component);
}
export function getComponent(name) {
return ComponentRegistry.get(name);
}
基于 Schema 的渲染器
function Renderer({ schema }) {
return schema.map((node) => {
const Component = getComponent(node.type);
return <Component key={node.id} {...node.props} />;
});
}
默认注册(核心模块)
registerComponent("field:text", TextField);
registerComponent("field:number", NumberField);
通过插件 / 第三方模块进行覆盖
registerComponent("field:text", CustomTextField);
没有编辑核心文件。没有分叉。也不需要为了证明 ERP 复杂而重建一个框架,只是因为它需要一个没有生态、工具或文档的复杂 UI 框架。
通过使用这种简单结构,至少可以实现 OWL.js 的几乎全部功能,同时还能利用现有生态系统的优秀工具。
关键优势
- 运行时替换
- 可控的扩展点
- 明确的覆盖所有权
- 可预测的行为
同样的模式可以扩展到:
- 权限
- 功能标记
- 用户特定的覆盖
- 基于上下文的渲染
- 更好的实现方式,处理菜单和 UI 翻译
这实际上就是 OWL 所做的——但 OWL 将其与自定义渲染引擎、生命周期模型和工具链捆绑在一起。
“但 React 不能进行运行时 UI 覆盖”
这是最常见的反对意见——而且它基于一种误解。
React 完全支持运行时 UI 覆盖。
它不支持的是隐式、无结构的变更——这是一种特性,而不是限制。
在 React 中实现运行时覆盖的方式包括:
- 注册表
- Context 提供者
- 依赖注入模式
- 插件系统
- 基于模式的渲染
所有这些都是:
- 显式的
- 可追踪的
- 可测试的
- 友好的工具链
OWL 的做法在很大程度上依赖于对 XML 的运行时解释以及隐式行为解析。这提供了灵活性——但代价是:
- 可调试性下降
- 静态分析受限
- 失败模式难以预测
React 生态系统更倾向于受控的可扩展性,而不是无限制的变更。这使得大型系统随着时间的推移更易维护,尤其是在多个团队和第三方开发者共同参与的情况下。
换句话说:
- OWL 优化的是最大化运行时自由度
- React 优化的是可持续的可扩展性
结束说明
这里的论点并不是说 OWL 不可用,也不是说 Odoo 开发者不了解现有框架。
论点在于 OWL 所解决的问题已经被解决,而重新构建一个框架来再次解决它会带来长期成本,却没有引入根本性的全新能力。
Odoo 并非仅仅选择了灵活性——它选择了拥有整个前端技术栈。拥有该栈意味着要承担随之而来的所有限制、漏洞、安全问题以及生态系统的缺口。
这才是重新发明 Web 技术栈的真实代价。这并不会使 Odoo 不可用——但确实让它的长期演进成本远高于本该有的水平。