全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版
Odoo v14引入了全新的JavaScript框架,名之为OWL (全称Odoo Web Library)。OWL是一个基于组件的框架,结构上使用QWeb模板。OWL与Odoo传统的我微件系统相比非常快速,并且引入了大量的功能,包括钩子、响应式、子组件自动实例化等等。本章中,我们将学习如何使用OWL组件生成交互UI元素。我们会通过OWL组件开始讲解,接着学习组件的生命周期。最后,我们会为视图新建整体上字段微件。本章将包含如下小节:
- 创建OWL组件
- 在OWL组件中管理用户动作
- 将OWL组件变为响应式
- 掌握OWL组件的生命周期
- 在表单视图中添加OWL字段
📝注:你可能会问以下的问题:Odoo为什么不使用那些知名的JavaScript框架,如React.js或Vue.js?可参见Odoo 14全新前端框架 OWL(Odoo Web Library)官方文档中文版了解更多有关OWL框架的知识。
OWL组件使用ES6类进行定义。本章中,我们会使用一些ES6语法。同时,老浏览器不支持某些ES6语法,所以请确保使用Chrome或Firefox的最新版本。本章中的代码参见 GitHub。
本节的主要目标是学习OWL组件的基础知识。我们会创建一个最小化的OWL组件并将其加入到Odoo网页客户端中。本节中我们创建一个带有文本的小横栏组件。
学习本需要用到带有基础字段和视图的my_library模块。可以在GitHub仓库的Chapter16/00_initial_module目录中找到这个基础my_library模块。
我们来对Odoo的网页端添加一个小的横栏。执行如下步骤来为Odoo客户端添加第一个组件:
-
添加一个JavaScript文件/my_library/static/src/js/component.js,定义新模块命名空间:
odoo.define('my.component', function (require) { "use strict"; // 在这里放第3-5步中的代码 });
-
添加XML文件/my_library/views/templates.xml file,并在资源中加载JavaScript组件如下:
<template id="assets_end" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> <script src="/my_library/static/src/js/component.js" type="text/javascript" /> </xpath> </template>
-
在第1步添加的component.js 文件中定义OWL工具类:
const { Component } = owl; const { xml } = owl.tags;
-
在第1步添加的component.js 文件中添加OWL组件及其基础模板:
class MyComponent extends Component { static template = xml` <div class="bg-info text-center p-2"> <b> Welcome to Odoo </b> </div>` }
-
初始化该组件并添加至网页客户端。在第1步添加的component.js 文件中添加如下代码:
owl.utils.whenReady().then(() => { const app = new MyComponent(); app.mount(document.body); });
安装/升级my_library模块来应用所做的修改。在Odoo中加载了我们的模块后,就可以看到下图所示的横栏:
图16.1 – OWL组件
这只是一个简单的组件。现在它还不会处理任何用户事件,也无法进行删除。
在第1和第2步中,我们添加了一个JavaScript文件并将其列举到后台资源中。如果希望学习更多有关静态资源的知识,请参见第十四章 CMS网站开发中管理静态资源一节。
第3步中,我们通过OWL初始化一个变量。OWL的所有工具类可以通过全局变量owl
进行获取。本例中,我们拉取了一个OWL工具类。首先声明了Component
,然后通过owl.tags
声明了xml
。Component
是OWL组件的主类,通过继承它可以创建我们自己的组件。
第4步中,我们通过继承OWL的 Component
类创建了自己的组件MyComponent
。为简化工作,我们只在MyComponent
类的定义中添加了QWeb模板。读者可能注意到我们使用xml
…``来声明模板。这一语法为行内模板。但可以通过单独文件来加载QWeb模板,通常也都是这么做的。我们在接下来的小节中会看到外部QWeb模板的示例。
📝**注:**行内QWeb模板不支持翻译或通过继承所做的修改。因此建议保持使用单独的文件来加载QWeb模板。
第5步中,我们实例化了MyComponent
组件并将其添加至body中。这个OWL组件是一个ES6类,因此可以通过关键字new
来创建一个对象。然后可以使用mount()
方法向页面添加该组件。如果留心的话,会发现我们将代码放到了whenReady()
回调方法内。这样可确保使用OWL组件前所有的OWL功能都正常地进行了加载。
OWL是一个单独的库,以外部JavaScript库的形式在Odoo内载入。也可以在其它的项目中使用OWL。有关OWL库请见https://github.com/odoo/owl。如果不希望在本机上测试OWL还可以使用在线的playground,在线使用OWL的地址为https://odoo.github.io/owl/playground。
为让用户界面更具交互性,组件需要处理如点击、悬停和表单提交这样的动作。本节中,我们会在组件中添加一个按钮,用于处理点击事件。
本节中我们将继续使用前一节中的my_library
模块。
本节中,我们会对组件添加一个删除按钮。在点击删除按钮时移除组件。执行如下步骤来在组件内添加删除按钮及删除事件:
-
更新QWeb模块,添加一个删除横栏的图标:
static template = xml` <div class="bg-info text-center p-2"> <b> Welcome to Odoo </b> <i class="fa fa-close p-1 float-right" style="cursor: pointer;" t-on-click="onRemove"> </i> </div>`
-
要删除该组件,对
MyComponent
类添加一个
onRemove
方法,如下:
class MyComponent extends Component { static template = xml` <div class="bg-info text-center p-2"> <b> Welcome to Odoo </b> <i class="fa fa-close p-1 float-right" style="cursor: pointer;" t-on-click="onRemove"> </i> </div>` onRemove(ev) { this.destroy(); } }
更新模块应用修改。更新完成后,会在横栏的右上角看到如下图所示的叉形图标:
图16.2 – 顶栏组件的删除按钮
点击删除按钮,会删除掉我们的OWL组件。重新加载后横栏会再次出现。
在第1步中,我们向组件添加了一个删除按钮。如果留意的话,会发现我们添加了一个t-on-click
属性。它会用于绑定点击事件。属性值为组件中的方法。在本例中,我们使用了t-on-click="onRemove"
。这表示在用户点击删除图标时,会调用组件中的onRemove
方法。定义事件的语法很简单:
t-on-<事件名>="<组件中的方法名>"
例如,如果希望在用户将鼠标移到组件上方时调用方法,可以通过如下代码实现:
t-on-mouseover="onMouseover"
在添加以上代码后,每当用户将鼠标移到组件上方时,OWL都会调用组件中所指定的onMouseover
方法。
在第2步中,我们添加了onRemove
方法。这个方法在用户点击删除图标时会被调用。方法内我们调用了destroy()
方法,它会从DOM中删除该组件。destroy()
方法内,我们接收JavaScript事件对象。destroy
是OWL组件的一个默认方法。在下面的小节中我们还会学习到其它的默认方法。
事件处理不只限于DOM事件。还可以使用自定义事件。例如,如果手动触发my-custom-event
事件,可以使用t-on-my-custom-event
来捕获自定义触发的事件。
OWL是一个强大的框架,支持根据钩子自动对UI进行更新。通过更新钩子,组件UI会在组件内部状态发生改变时自动更新。本节中,我们会在组件内根据用户动作更新消息内容。
本节中,我们将继续使用前一小节中的my_library
模块。
本节中,我们在组件内文本的两边添加上箭头。点击箭头时更改消息内容。执行如下步骤来让这个OWL组件具有交互性:
-
更新组件的XML模板。在文本两边添加带有事件指令的两个按钮。同时从列表中动态获取消息:
static template = xml` <div class="bg-info text-center p-2"> <i class="fa fa-arrow-left p-1" style="cursor: pointer;" t-on-click="onPrevious"> </i> <b t-esc="messageList[Math.abs( state.currentIndex%4)]"/> <i class="fa fa-arrow-right p-1" style="cursor: pointer;" t-on-click="onNext"> </i> <i class="fa fa-close p-1 float-right" style="cursor: pointer;" t-on-click="onRemove"> </i> </div>`
-
在组件的JavaScript文件中导入
useState
钩子如下:
const { Component, useState } = owl;
-
对组件添加
constructor
方法,初始化变量如下:
constructor() { super(...arguments); this.messageList = [ 'Hello World', 'Welcome to Odoo', 'Odoo is awesome', 'You are awesome too' ]; this.state = useState({ currentIndex: 0 }); }
-
在
Component
类中,添加处理用户点击事件的方法:
onNext(ev) { this.state.currentIndex++; } onPrevious(ev) { this.state.currentIndex--; }
图16.3 – 文本两边添加箭头
如果点击箭头,消息文本会根据构造函数中的消息列表进行改变。
第1步中,我们更新了组件中的XML模板。基本上是对模板做了两处修改。我们通过消息列表渲染文本消息,根据状态变量中currentIndex
的值来选取消息。我们在文本块两边添加了箭头图标。在箭头图标内,添加了t-on-clic
属性来对箭头绑定点击事件。
第2步中,我们通过OWL导入了useState
钩子。这个钩子用于处理组件的状态。第3步中,我们添加了构造函数。在创建对象实例时会调用这一构造函数。在构造函数内,我们添加了希望展示的消息列表,然后使用useState
钩子添加了状态变量。这会使得组件成为响应式。在状态发生改变时,UI会按照新的状态进行更新。本例中,我们在useState
钩子内使用了currentIndex
。这表示在currentIndex
的值发生变化时,UI也会进行更新。
📝**重要信息:**定义钩子只有一条规则,即仅在构造函数时声明的钩子有效。还有其它类型的钩子,请参见官方文档。
第4步中,我们添加了处理箭头点击事件的方法。在点击箭头时我们会改变组件的状态。因为对状态使用了钩子,组件的UI会自动进行更新。
OWL组件拥有一些有助于开发者创建强大、交互性组件的方法。本节中,我们学习这些组件的一些重要方法以及这些方法调用所处的生命周期。本节中我们会对组件添加一些方法,并会在控制台中打印消息来协助理解组件的生命周期。
本节中,我们将继续使用前一小节中的my_library
模块。
需要执行如下步骤来对组件添加生命周期方法:
-
在组件中已经有构造方法了,我们对控制台添加消息如下:
constructor() { console.log('CALLED:> constructor'); ...
-
在组件中添加
willStart
方法:
async willStart() { console.log('CALLED:> willStart'); }
-
在组件中添加
mounted
方法:
mounted() { console.log('CALLED:> mounted'); }
-
在组件中添加
willPatch
方法:
willPatch() { console.log('CALLED:> willPatch'); }
-
在组件中添加
patched
方法:
patched() { console.log('CALLED:> patched'); }
-
在组件中添加
willUnmount
方法:
willUnmount() { console.log('CALLED:> willUnmount'); }
重启、升级模板来应用模板的修改。在升级后执行一些操作,比如通过箭头修改消息内容、删除组件。在浏览器的控制台中会看到如下的消息:
图16.4 – 浏览器控制台中的消息记录
对组件执行不同的操作会得到不一样的日志记录。
本节中我们添加了一些方法并对方法添加了一些日志消息。可以根据自己的需求来使用这些方法。我们来了解下组件的生命周期以及何时调用这些方法。
constructor()
: 构造方法在组件生命周期中第一个进行调用。它在组件初始化时调用。我们需要在此处设置组件的初始状态。
willStart()
: willStart
方法在构造方法之后及元素渲染之前调用。它是一个异步方法,可以执行一些像RPC这样的异步操作。
mounted()
: mounted
方法在渲染组件、添加DOM之后调用。
willPatch()
: willPatch
方法在组件状态发生改变时调用。此方法在元素根据新状态重新渲染之前进行调用。本例中,该方法在点击箭头时调用。但在这个方法调用之时, DOM使用的还是老值。
patched()
: patched
方法类似于 willPatch
方法。会在状态发生变化时调用,唯一的区别是patched
方法在元素根据新状态重新渲染之后进行调用。
willUnmount()
: willUnmount
方法在元素从DOM中移除之前调用。本例中,此方法在点击删除图标进行组件移除时调用。
这些就是组件的生命周期方法,开发者需要根据自己的需求来使用。例如,mounted
和 willUnmount
方法可用于绑定及解绑事件监听器。
在组件生命周期中还有一个方法,但是在用子组件时使用。OWL通过props
参数传递父组件的状态,在props
发生改变时,会调用willUpdateProps
方法。这是一个异步方法,也就是说可以在其中执行RPC这样的异步操作。
至此,我们学习有关OWL的基础知识。现在我们会进行更高级的内容,创建一个可在表单视图中使用的字段微件,就像前一章字段微件那一节那样。本节中,我们将创建一个颜色拾取器我邮件,它根据所选取的颜色保存不同的整数值。
为使用本例更具知识性,我们将使用一些高级OWL概念。我们会使用到多组件、自定义事件、外部QWeb模板等。
本节中,我们将继续使用前一小节中的my_library
模块。
-
对
library.book
模型添加一个整型字段color:
color = fields.Integer()
-
同时在表单视图中添加一个带有widget属性的相同字段:
<field name="color" widget="int_color"/>
-
在
static/src/xml/qweb_template.xml
中添加该字段的QWeb模板:
<?xml version="1.0" encoding="UTF-8"?> <templates> <t t-name="OWLColorPill" owl="1"> <span t-attf-class="o_color_pill o_color_{{props.pill_no}} {{props.active and 'active' or ''}}" t-att-data-val="props.pill_no" t-on-click="pillClicked" t-attf-title="This color is used in {{props.book_count or 0 }} books." /> </t> <span t-name="OWLFieldColorPills" owl="1" class="o_int_colorpicker" t-on-color-updated="colorUpdated"> <t t-foreach="totalColors" t-as='pill_no'> <ColorPill t-if="mode === 'edit' or value == pill_no" pill_no='pill_no' active='value == pill_no' book_count="colorGroupData[pill_no]"/> </t> </span> </templates>
-
在模块的
manifest
文件中列举该QWeb文件:
"qweb": [ 'static/src/xml/qweb_template.xml', ],
-
然后在
static/src/scss/field_widget.scss
中添加字段的一些SCSS样式。因SCSS的内容较称,请参见GitHub仓库相应的SCSS文件内容。 -
添加JavaScript文件
static/src/js/field_widget.js
并加入如下基础内容:
odoo.define('my_field_widget', function (require) { "use strict"; const { Component } = owl; const AbstractField = require('web.AbstractFieldOwl'); const fieldRegistry = require('web.field_registry_owl'); // 将7-8步的代码放在这里 });
-
在
field_widget.js
中,添加色块组件如下:
class ColorPill extends Component { static template = 'OWLColorPill'; pillClicked() { this.trigger('color-updated', {val: this.props.pill_no}); } }
-
在
field_widget.js
中,通过继承
AbstractField
添加字段颜色组件如下:
class FieldColor extends AbstractField { static supportedFieldTypes = ['integer']; static template = 'OWLFieldColorPills'; static components = { ColorPill }; // 添加第9步中的方法 } fieldRegistry.add('int_color', FieldColor);
-
将给定的方法添加到第8步所创建的
FieldColor
中:
constructor(...args) { super(...args); this.totalColors = Array.from({length: 10}, (_, i) => (i + 1).toString()); } async willStart() { this.colorGroupData = {}; var colorData = await this.rpc({ model: this.model, method: 'read_group', domain: [], fields: ['color'], groupBy: ['color'], }); colorData.forEach(res => { this.colorGroupData[res.color] = res.color_count; }); } colorUpdated(ev) { this._setValue(ev.detail.val); }
-
将JavaScript和SCSS文件添加到后台资源中如下:
<template id="assets_backend" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> <script src="/my_library/static/src/js/component.js" type="text/javascript" /> <script src="/my_library/static/src/js/field_widget.js" type="text/javascript" /> <link href="/my_library/static/src/scss/field_widget.scss" rel="stylesheet" type="text/scss" /> </xpath> </template>
重启、更新模块来应用所做的修改。以编辑模式打开图书的表单视图。就会看到如下图这样的颜色拾取器微件:
图16.5 – 颜色拾取器OWL微件
该字段和上一章中的颜色微件一样,但实际上底层是不一样的。新字段使用OWL构建,而前一章使用微件构建。
第1步中,我们向library.book
模型添加了一个整型字段。第2步中,我们对书籍的表单视图添加了一个字段。
第3步中,我们添加了QWeb模板文件。如果留意的话,我们在文件中添加了两个模板,一个用于色块,另一个用于字段本身。使用两个模板原因在于希望一探子组件的概念。如果仔细观察模板,会发现我们使用了<ColorPill>
标签。它会用于实例化子组件。在<ColorPill>
标签中,我们传递了active
和 pill_no
属性。这两个属性会在子组件的模板中以props
进行接收。同时注意t-on-color-updated
属性用于监听子组件所触发的自定义事件。
📝**重要信息:**Odoo v14同时使用微件系统和OWL框架。两者均使用QWeb模板,但为区分OWL QWeb模板和传统的QWeb模板,需要在模板定义中使用
owl="1
。
第4步中,我们在声明文件中列举了自己的QWeb模板。它在自动在浏览器中加载我们的模板。
第5步中,我们对颜色添加了SCSS样式文件。这有助于让我们的颜色拾取器UI更为美观。
第6步中,我们为字段组件添加了JavaScript。导入了OWL工具类,并且同时导入了AbstractField
和fieldRegistry
。AbstractField
是字段的抽象OWL组件。它包含创建字段所需的所有基础元素。fieldRegistry
用于将OWL组件列举为字段组件。
第7步中,我们创建了ColorPill
组件。组件中的template
变量为通过外部XML文件加载的模板名称。ColorPill
组件带有pillClicked
方法,在用点击颜色块时进行调用。在方法体内部,我们触发了color-updated
事件,它会由父组件FieldColor
进行捕获,因为我们对FieldColor
组件使用了t-on-color-updated
。
第8和第9步中,我们通过继承AbstractField
创建了FieldColor
组件。使用AbstractField
组件是因为它拥有创建字段微件所需要的所有工具方法。细心的读者会发现我们在开头使用了静态变量components
。在模板中使用子组件时需要通过components
静态变量列出组件。我们还在示例中添加了willStart
方法。willStart
是一个异步方法,因此我们调用了RPC(网络调用)来获取某一姿色图书的数量。然后,我们添加了colorUpdated
方法,它会在用户点击色块时进行调用。因此我们在修改字段值。setValue
方法用于设置字段值(它会存储到数据库中)。注意这里子组件所触发的数据在event
参数的detail
属性中可进行获取。最后,我们在fieldRegistry
中注册了自己的微件,表示至此我们就可以在表单视图中通过widget
属性来使用我们的字段了。
第10步中,我们将JavaScript和SCSS文件加载到后台静态资源中。