我相信您熟悉数百行用于查找或操作 HTML 元素的 javascript。同时也做一些事情,比如跟踪事件侦听器绑定逻辑。这些框架消除了几乎所有繁重的工作,并将您的 HTML 和 javascript 更紧密地结合在一起。
但在某些时候,我们作为前端开发人员共同同意,将 HTML 和 javascript 结合起来实际上很有意义!它们本质上是相互依赖的。您可以编写并在其他不依赖 HTML 和浏览器的项目中重用纯的、无副作用的 javascript 代码非常少。因此,如果我们坚持使用这两种技术,那么让我们接受它并在这里关注可用性而不是模块化。
const { Component, useState } = owl;
class ClickComponent extends Component {
state = useState({ count: 0 });
}
就是这样。我们跟踪一些数据和所有的事件监听器,DOM 操作的混乱会自动为我们处理。很酷。
- Odoo 为什么要创建 OWL?
我不为 Odoo 工作,也没有参与创建 OWL,所以我不会过多谈论这个。总的来说,我知道他们想要改进他们的前端代码并实现一个现代框架。
如果您想知道为什么他们创建了一个全新的框架而不是使用现有框架,我建议您 通读他们针对该确切问题的文章。
三、主要特点
就个人而言,我认为 OWL 与处理前端开发的旧方法相比有 3 个主要好处。所有这些功能都以更优雅的方式实现,我非常喜欢任何能让我编写更简单、更优雅、更易读的代码的东西。
3.1. 生命周期组件
我过去用 javascript 编写的很多代码都有大量的样板代码来管理 DOM 的状态。我们需要不断考虑前端元素可能发生的所有事情,以确保我们的代码不会中断。
拥有具有生命周期和状态的组件是一个巨大的好处。我们知道,当页面加载我们的组件启动时,当页面重定向时,我们的组件可能会崩溃并消失。
我们可以为所有这些事情提供钩子。不再有$(document).ready 过去。
3.2. 与虚拟 dom 的反应式绑定
如果你参考我在介绍中开始的小例子,你会看到一个反应式绑定的例子。我们只需要考虑数据,数据将显示在哪里,以及数据将在何时被操作。所有的 DOM 操作代码都被抽象出来了。更改数据,前端会自动更新。简而言之,这就是反应式绑定。
有点相关,许多现代前端框架都提供了虚拟 DOM。这意味着他们在 javascript 中跟踪前端结构,主要是在幕后。这对开发人员进行调试等工作非常有帮助。我会在以后的文章中深入探讨这一点。
3.3. 可读性
我们已经有足够的时间来跟踪编写 javascript 了。添加手动跟踪事件侦听器和回调之类的需要只是为了简单地更新前端,可能会一团糟。但是消除这种负担使我们能够编写更好的代码、更具可读性的代码并处理更复杂的情况。
这对开发人员来说似乎微不足道,但在我的软件职业生涯中,成功地生成易于理解的代码可以让你在其他一切方面都有优势。代码更容易测试。代码更容易维护。代码更少错误。只是通过关注易于理解的代码,这个列表会继续下去。
PS:如果你还没有听过DHH 关于软件编写的演讲,我强烈推荐它。
- 入门
本指南假设您了解一些 Odoo 开发的基本知识。如果您以前从未使用过 Odoo,您可能需要查看我们的其他一些文章,获取本地开发环境设置,并了解系统。
OWL 被打包到 Odoo 14 及更高版本中。在本文中,我想专注于在 Odoo 中具体使用 OWL,而不是作为一个独立的前端框架。
与 Odoo 开发一样,我们必须有一个新模块可以使用。让我们设置最简单的一个,以便我们可以尝试一些 OWL 功能。
intro_to_owl_part_1/
├── __manifest__.py
└── static
└── src
└── js
└── components
# __manifest__.py
{
"name": "Introduction to OWL in Odoo - Part 1",
"summary": "Provides an example module for OWL.",
"description": "Provides an example module for OWL.",
"author": "Oocademy",
"website": "http://www.oocademy.com",
"category": "Tutorials",
"version": "14.0.0.1",
"depends": ["base"],
"demo": [],
"data": [],
}
- 添加我们的第一个组件
OWL 通过定义Component类来工作,这些类可以被认为是 Web 组件。每个组件都以一个模板、绑定到该模板的数据以及任何子组件开始。
在 HTML 中,我们有所有这些标签,如header、div、 span、textarea等。在使用 OWL 时,您需要考虑“如果我可以创建一个全新的标签,对项目有什么用?”。所以也许你需要一个projectandtask 标签来显示项目管理系统的信息。或用于在您的软件系统中显示用户/客户的联系人标签。
在本文中,我们要创建一个组件,该组件显示在销售订单上的客户下方,该组件显示有关其订单历史记录的一些详细信息。
要将新组件添加到模块中,您需要 2 个步骤:
5.1. 第一步:创建并注册js类
将调用我们的新组件文件PartnerOrderSummary.js,并将其添加到static/src/js/components/.
odoo.define("intro_to_owl_part_1.PartnerOrderSummary", function (require) {
const { Component } = owl;
class PartnerOrderSummary extends Component {
//
};
Object.assign(PartnerOrderSummary, {
template: "intro_to_owl_part_1.PartnerOrderSummary"
});
});
Odoo 中的所有 javascript 文件都需要通过扩展资产模板和添加script标签来注册。在这种情况下,让我们创建一个assets.xml在模块根目录中调用的文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend_inherit" inherit_id="web.assets_backend">
<xpath expr="script[last()]" position="after">
<script src="/intro_to_owl_part_1/static/src/js/components/PartnerOrderSummary.js"/>
</xpath>
</template>
</odoo>
然后assets.xml需要将其添加到我们的模块中__manifest__.py。
{
...
"data": [
"assets.xml"
]
}
5.2. 第 2 步:为组件创建模板 xml
现在让我们创建我们的模板 XML。我关注了 js 类的一些细节(我保证我会循环回看),但我确定您已经注意到template添加到我们类中的属性。
class PartnerOrderSummary extends Component {
//
};
Object.assign(PartnerOrderSummary, {
template: "intro_to_owl_part_1.PartnerOrderSummary"
});
在template引用了一个XML模板名称。我喜欢在同一个文件夹中组织我的组件,所以让我们在static/src/js/components/PartnerOrderSummary.xml我们的 js 类旁边添加一个带有基本模板的文件:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="intro_to_owl_part_1.PartnerOrderSummary" owl="1">
<div>My cool new widget</div>
</t>
</templates>
与我们的 js 类类似,我们需要将它注册到模块中。对于这些模板,我们只需将其添加到文件中的qweb 配置中__manifest__.py。
{
...
"qweb": [
"static/src/js/components/PartnerOrderSummary.xml"
]
}
- 在销售订单上显示我们的组件
所以我们这里有一个带有组件的小模块,现在可以显示一些文本。但是我们需要它实际显示在销售订单视图表单上,对吗?
6.1. 更新依赖
首先,我们需要更新模块依赖项以包含销售模块:
{
..
"depends": ["sale", "sale_management"],
}
6.2. 覆盖表单渲染器以将组件挂载到类
在更新您的模块以安装 Sales 之后,让我们将该组件添加到销售订单表单中。
有多种方法可以解决这个问题。我将向您展示我认为最简单的方法,即在页面加载时将组件安装到视图中。
我们将更新我们的js文件以扩展核心 Odoo 表单渲染器以查找某个 html 类并自动将我们的组件挂载到该元素。
odoo.define(“intro_to_owl_part_1.PartnerOrderSummary”, function
(require) {
const FormRenderer = require("web.FormRenderer");
const { Component } = owl;
const { ComponentWrapper } = require("web.OwlCompatibility");
/**
* OWL component responsible for displaying a partner order summary widget
* which will show order history details about a specific customer.
*/
class PartnerOrderSummary extends Component {
//
};
/**
* Register properties to our widget.
*/
Object.assign(PartnerOrderSummary, {
template: "intro_to_owl_part_1.PartnerOrderSummary"
});
/**
* Override the form renderer so that we can mount the component on render
* to any div with the class o_partner_order_summary.
*/
FormRenderer.include({
async _render() {
await this._super(...arguments);
for(const element of this.el.querySelectorAll(".o_partner_order_summary")) {
(new ComponentWrapper(this, PartnerOrderSummary))
.mount(element)
}
}
}); });
这里有很多东西需要学习,特别是如果你没有在 Odoo 中做过很多前端工作。在本文中,我不会深入探讨这些东西在幕后是如何工作的。这里的主要内容是有一种方法可以将 OWL 组件安装到元素上。在我们这里的例子中,我们将挂载到一个具有 class 的元素o_partner_order_summary。
我们可以非常简单地通过以下方式将组件挂载到任何元素对象:
(new ComponentWrapper(this, PartnerOrderSummary))
.mount(element)
我们可以在多个地方连接到核心 Odoo 类,但由于我们将组件添加到销售订单表单视图中,我发现最简单的方法是覆盖web.FormRendererclass.
6.3. 将 div 添加到销售订单表单
现在让我们覆盖销售订单视图并添加我们的 div。我通过views.xml在我们模块的根目录中创建 a来做到这一点:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="payment_term_id" position="after">
<div class="o_partner_order_summary" colspan="2"/>
</field>
</field>
</record>
</odoo>
像往常一样使用 XML 文件,更新我们的__manifest__.py.
{
...
"data": [
...
"views.xml",
],
}
- 扩展我们的小部件
此时,您应该会看到您的酷炫新模块!尽管还不是很酷,但至少困难的部分已经完成。
销售订单简单小部件示例
7.1. 设计我们的小部件
让我们的小部件看起来更好。我们可以在链接数据之前模拟前端。
<?xml version="1.0" encoding="UTF-8"?>
<!-- Customer name -->
<p style="font-size: 16px; color: #4d4b4b;"><strong>Gemini Furniture</strong></p>
<!-- Address -->
<p style="font-size: 12px; color: #8c8787;">
<i class="fa fa-map-marker" style="padding-right: 4px;"/>
<span>Fairfield</span>
</p>
<!-- Grid of previous order stats -->
<div class="row" style="padding-top: 20px;">
<div class="col-6" style="border-right: 1px solid #ccc;">
<p style="font-size: 20px;"><strong>35</strong></p>
<p style="font-size: 12px; color: #8c8787;">Orders</p>
</div>
<div class="col-6">
<p style="font-size: 20px;"><strong>$97,183.50</strong></p>
<p style="font-size: 12px; color: #8c8787;">Total Sales</p>
</div>
</div>
</div>
</t>
``` ```
显然,此时我们正在使用虚拟数据,但这就是我们的小部件在重新加载更新后的 XML 后应该如何处理的。这些当前的更改没有什么特别之处,因为它几乎只是 HTML 和内联样式(如果您知道如何,我建议在 Odoo 中创建一个单独的样式表)。
没有数据示例的销售订单小部件模拟
7.2. 连接数据
此时的最后一步是实际链接我们的合作伙伴数据。OWL 通过状态对象跟踪组件上的所有数据。因此,我们将首先添加一个合作伙伴属性并允许一种设置构造数据的方法。
const { useState } = owl.hooks;
class PartnerOrderSummary extends Component {
partner = useState({});
constructor(self, partner) {
super();
this.partner = partner;
}
}
当表单视图呈现时,组件对象实际上会被初始化。此时,我们可以从销售订单中获取合作伙伴信息并将其传递给我们的构造函数:
FormRenderer.include({
async _renderView() {
await this._super(...arguments);
for(const element of this.el.querySelectorAll(".o_partner_order_summary")) {
this._rpc({
model: "res.partner",
method: "read",
args: [[this.state.data.partner_id.res_id]]
}).then(data => {
(new ComponentWrapper(
this,
PartnerOrderSummary,
useState(data[0])
)).mount(element);
});
}
}
});
让我们分解一下这里发生了什么。首先,我们在页面中查找具有我们的类 o_partner_order_summary 的元素。然后我们对后端进行 rpc 调用,以从链接到当前订单(this.state.data代表当前记录)的合作伙伴那里获取所有数据。一旦数据返回,我们就开始安装我们的组件。
此时,我们可以通过 XML 视图中的合作伙伴变量获得合作伙伴记录中的所有数据。
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="intro_to_owl_part_1.PartnerOrderSummary" owl="1">
<div class="center" style="width: 100%; text-align: center; border: 1px solid #cecece; padding: 2rem 20%; margin: 12px 0;">
<img
t-attf-src="data:image/jpg;base64,{{partner.image_256}}"
width="75px"
height="75px"
style="background-color: #ccc; border-radius: 50%; margin-bottom: 10px;"/>
<!-- Customer name -->
<p style="font-size: 16px; color: #4d4b4b;"><strong t-esc="partner.name"/></p>
<!-- Address -->
<p style="font-size: 12px; color: #8c8787;">
<i class="fa fa-map-marker" style="padding-right: 4px;"/>
<span t-esc="partner.city"/>
<span t-esc="partner.zip" style="margin-left: 5px;"/>
</p>
<!-- Grid of previous order stats -->
<div class="row" style="padding-top: 20px;">
<div class="col-6" style="border-right: 1px solid #ccc;">
<p style="font-size: 20px;">
<strong t-esc="partner.sale_order_count"/>
</p>
<p style="font-size: 12px; color: #8c8787;">Orders</p>
</div>
<div class="col-6">
<p style="font-size: 20px;">
<strong t-esc="partner.sale_order_revenue" t-options='{"widget": "monetary"}'/>
</p>
<p style="font-size: 12px; color: #8c8787;">Total Sales</p>
</div>
</div>
</div>
</t>
</templates>
我们的新视图遍历并用t-语句替换我们所有的虚拟数据, 以从合作伙伴那里提取我们需要的任何数据。要使用的最简单的属性是t-esc将数据打印到 div。
要修改属性,我们可以t-attf-在属性名称之前添加,然后访问该字符串中的变量。
例如,我们可以像这样显示图像:
<img
t-attf-src="data:image/jpg;base64,{{ partner.image_256 }}"
width="75px"
height="75px"
style="background-color: #ccc; border-radius: 50%; margin-bottom: 10px;"/>
或者我们可以像这样显示合作伙伴的 ciy 和 zip:
<p style="font-size: 12px; color: #8c8787;">
<i class="fa fa-map-marker" style="padding-right: 4px;"/>
<span t-esc="partner.city"/>
<span t-esc="partner.zip" style="margin-left: 5px;"/>
</p>
当我们现在更新模块并转到销售订单时,我们可以看到带有客户详细信息的自定义小部件: 销售订单完成小部件示例客户 1
- 总结
我已经尝试向您介绍在 Odoo 中设置 OWL 组件的所有绝对基础知识,注册您需要的所有文件和类,创建一个简单的组件,将该组件链接到现有的 Odoo 视图,存储组件数据,并呈现组件数据。
Odoo 中的 OWL 和前端开发还有很多我们计划在以后的文章中解决。请随时与我们联系,让我们知道您还想知道什么!
我希望这可以帮助您的 Odoo 开发人员并祝您编码好运!
参考文档:https://github.com/odoo/owl
https://odoo.github.io/owl/playground/