第 1 章:Owl 组件¶
本章介绍 Owl 框架,这是为 Odoo 定制的组件系统。OWL 的主要构建块是 组件 和 模板。
在 Owl 中,用户界面的每个部分都由一个组件来管理:它们包含逻辑并定义用于渲染用户界面的模板。实际上,一个组件是由一个继承自 Component
类的小型 JavaScript 类表示的。
要开始,请确保你有一个正在运行的 Odoo 服务器和一个开发环境。在开始练习之前,请确保你已经按照此 教程简介 中描述的所有步骤进行了操作。
在本章中,我们使用 awesome_owl
插件,它提供了一个简化的环境,仅包含 Owl 和一些其他文件。目标是学习 Owl 本身,而不依赖 Odoo 网页客户端代码。
每个章节练习的解决方案托管在 官方 Odoo 教程仓库 中。建议先尝试自己解决问题,不要直接查看解决方案!
示例:一个 Counter
组件¶
首先,让我们来看一个简单的例子。下面所示的 Counter
组件是一个维护内部数字值的组件,它会显示该值,并在用户点击按钮时更新该值。
import { Component, useState } from "@odoo/owl";
export class Counter extends Component {
static template = "my_module.Counter";
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
Counter
组件指定了一个用于表示其 HTML 的模板名称。它使用 QWeb 语言以 XML 格式编写:
<templates xml:space="preserve">
<t t-name="my_module.Counter">
<p>Counter: <t t-esc="state.value"/></p>
<button class="btn btn-primary" t-on-click="increment">Increment</button>
</t>
</templates>
1. 显示计数器¶

作为第一个练习,让我们修改位于 awesome_owl/static/src/
的 Playground
组件,将其变成一个计数器。要查看结果,您可以在浏览器中访问 /awesome_owl
路线。
修改
playground.js
,使其像上面的例子一样作为一个计数器。保持类名为Playground
。你需要使用 useState hook,这样当该组件读取的状态对象的任何部分被修改时,组件都会重新渲染。在同一个组件中,创建一个
increment
方法。修改
playground.xml
中的模板,使其显示你的计数器变量。使用 t-esc 来输出数据。在模板中添加一个按钮,并在按钮中指定 t-on-click 属性,以便在点击按钮时触发
increment
方法。
小技巧
Odoo 浏览器下载的 JavaScript 文件是经过压缩的。为了便于调试,当文件未被压缩时会更容易。切换到 通过资源启用调试模式,以便文件不被压缩。
这个练习展示了 Owl 的一个重要特性:反应式系统。useState
函数会将一个值包装在一个代理中,这样 Owl 就可以跟踪哪个组件需要状态的哪一部分,以便在值发生变化时进行更新。尝试移除 useState
函数,看看会发生什么。
2. 在子组件中提取 Counter
¶
目前我们已经在 Playground
组件中实现了计数器的逻辑,但它无法被复用。让我们看看如何从中创建一个 子组件:
从
Playground
组件中提取计数器代码,创建一个新的Counter
组件。你可以在同一个文件中先完成,但完成后,更新你的代码,将
Counter
移动到它自己的文件夹和文件中。从./counter/counter
相对导入它。确保模板也在它自己的文件中,且名称相同。在
Playground
组件的模板中使用<Counter/>
,以在你的实验区添加两个计数器。

小技巧
按照惯例,大多数组件的代码、模板和 CSS 应该与组件具有相同的蛇形命名。例如,如果我们有一个 TodoList
组件,其代码应位于 todo_list.js
、todo_list.xml
中,如有需要,还可以有 todo_list.scss
。
3. 一个简单的 Card
组件¶
组件是将复杂的用户界面划分为多个可重用部分的最自然方式。但要使它们真正有用,有必要能够在它们之间传递一些信息。让我们看看如何通过属性(通常称为 props)让父组件向子组件提供信息。
本次练习的目标是创建一个 Card
组件,它接收两个属性:title
和 content
。例如,以下是它的使用方式:
<Card title="'my title'" content="'some content'"/>
上面的示例应使用 Bootstrap 生成一些 HTML,外观如下:
<div class="card d-inline-block m-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">my title</h5>
<p class="card-text">
some content
</p>
</div>
</div>
创建一个
Card
组件在
Playground
中导入它,并在其模板中显示几张卡片

4. 使用 markup
显示 HTML¶
如果你在之前的练习中使用了 t-esc
,你可能会注意到 Owl 会自动转义其内容。例如,如果你尝试这样显示一些 HTML:<Card title="'my title'" content="this.html"/>
,其中 this.html = "<div>some content</div>"
,那么最终的输出将只是将 HTML 作为字符串显示出来。
在这种情况下,由于 Card
组件可能用于显示任何类型的内容,允许用户显示一些 HTML 是合理的。这是通过 t-out 指令 实现的。
然而,将任意内容作为 HTML 显示是危险的,可能会被用来注入恶意代码,因此默认情况下,Owl 会始终转义字符串,除非它已被明确使用 markup
函数标记为安全。
将
Card
更新为使用t-out
将
Playground
更新为导入markup
,并在某些 HTML 值中使用它确保您看到普通字符串始终会被转义,而标记字符串则不会。
注解
t-esc
指令仍然可以在 Owl 模板中使用。它比 t-out
稍微快一些。

5. 属性验证¶
Card
组件有一个隐式的接口。它期望通过 props 接收两个字符串:title
和 content
。让我们让这个接口更加明确。我们可以添加一个 props 定义,这将让 Owl 在 开发模式 中执行验证步骤。你可以在 应用配置 中激活开发模式(但在 awesome_owl
演示环境中默认已启用)。
为每个组件进行属性验证是一个良好的实践。
在
Card
组件中添加 props 验证。在游乐场模板中将
title
属性重命名为其他名称,然后在浏览器开发者工具的 控制台 选项卡中检查是否能看到错误信息。
6. 两个 Counter
的总和¶
我们之前在练习中看到,props
可以用于从父组件向子组件传递信息。现在,让我们看看如何反向传递信息:在这个练习中,我们希望显示两个 Counter
组件,并在它们下方显示它们值的总和。因此,父组件(Playground
)需要在任何一个 Counter
的值发生变化时得到通知。
可以通过使用 回调属性:一个用于被调用的函数属性。子组件可以选择使用任何参数来调用该函数。在我们的例子中,我们将简单地添加一个可选的 onChange
属性,该属性将在 Counter
组件增加时被调用。
为
Counter
组件添加属性验证:它应该接受一个可选的onChange
函数属性。将
Counter
组件更新为在每次递增时调用onChange
属性(如果存在的话)。将
Playground
组件修改为维护一个本地状态值(sum
),初始值设为 2,并在其模板中显示它在
Playground
中实现incrementSum
方法将该方法作为属性传递给两个(或更多)子
Counter
组件。

重要
带有回调属性的细微之处在于:它们通常应该使用 .bind
后缀来定义。参见 文档。
7. 待办事项列表¶
现在让我们通过创建一个待办事项列表来了解 Owl 的各种功能。我们需要两个组件:一个 TodoList
组件,用于显示多个 TodoItem
组件。待办事项的列表是一个状态,应由 TodoList
来维护。
For this tutorial, a todo
is an object that contains three values: an id
(number), a description
(string) and a flag isCompleted
(boolean):
{ id: 3, description: "buy milk", isCompleted: false }
创建
TodoList
和TodoItem
组件。TodoItem
组件应该接收一个todo
作为属性,并在div
中显示其id
和description
。目前,硬编码待办事项列表:
// in TodoList this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
使用 t-foreach 在
TodoItem
中显示每个待办事项。在沙盒中显示
TodoList
。为
TodoItem
添加属性验证。

小技巧
由于 TodoList
和 TodoItem
组件耦合度非常高,将它们放在同一个文件夹中是合理的。
注解
t-foreach
指令在 Owl 中与 QWeb 的 Python 实现并不完全相同:它需要一个 t-key
唯一值,以便 Owl 能够正确地对齐每个元素。
8. 使用动态属性¶
目前,TodoItem
组件在视觉上无法显示 todo
是否已完成。让我们通过使用 动态属性 来实现这一点。
如果
TodoItem
根元素已完成后,添加 Bootstrap 类text-muted
和text-decoration-line-through
。将硬编码的
this.todos
值更改为检查其是否正确显示。
尽管该指令名为 t-att`(表示属性),但它可以用于设置 `class
值(以及 HTML 属性,如输入框的 value
)。

9. 添加待办事项¶
到目前为止,我们列表中的待办事项是硬编码的。让我们通过允许用户向列表中添加待办事项来使其更加有用。
在
TodoList
组件中移除硬编码的值:this.todos = useState([]);
在任务列表上方添加一个输入框,占位符为“输入新任务”。
在
keyup
事件上添加一个 event handler,名为addTodo
。实现
addTodo
方法,用于检查是否按下了回车键(ev.keyCode === 13
),如果是,则使用输入框的当前内容作为描述创建一个新的待办事项,并清空输入框中的所有内容。确保待办事项具有唯一的 ID。可以只是一个每次添加待办事项时递增的计数器。
加分点:如果输入为空,则不要做任何操作。

另请参见
理论:组件生命周期和钩子¶
到目前为止,我们已经看到了一个钩子函数的示例:useState
。一个 hook 是一个特殊的函数,它 钩入 组件的内部机制。在 useState
的情况下,它会生成一个与当前组件相关联的代理对象。这就是为什么钩子函数必须在 setup
方法中调用,而不能在之后调用的原因!
Owl 组件会经历许多阶段:它可以被实例化、渲染、挂载、更新、分离、销毁……这就是 组件生命周期。上图显示了组件生命周期中最重要的事件(钩子以紫色显示)。大致来说,一个组件会被创建,然后被更新(可能多次),最后被销毁。
Owl 提供了多种内置的 钩子函数。所有这些钩子函数都必须在 setup
函数中调用。例如,如果您希望在组件挂载时执行某些代码,可以使用 onMounted
钩子:
setup() {
onMounted(() => {
// do something here
});
}
小技巧
所有钩子函数都以 use
或 on
开头。例如:useState
或 onMounted
。
10. 聚焦输入¶
让我们看看如何通过 t-ref 和 useRef 访问 DOM。主要思想是需要在组件模板中用 t-ref
标记目标元素:
<div t-ref="some_name">hello</div>
然后你可以在 JS 中通过 useRef 钩子 来访问它。然而,如果你仔细想想,这里有一个问题:组件在创建时,其实际的 HTML 元素并不存在。只有在组件挂载后,该元素才会存在。但是,钩子必须在 setup
方法中调用。因此,useRef
返回一个包含 `el`(表示元素)键的对象,该键仅在组件挂载后才会被定义。
setup() {
this.myRef = useRef('some_name');
onMounted(() => {
console.log(this.myRef.el);
});
}
聚焦上一练习中的
input
。这应该在TodoList
组件中完成(请注意,输入 HTML 元素上有一个focus
方法)。加分项:将代码提取到一个新的
awesome_owl/utils.js
文件中的专用 hookuseAutofocus
。

小技巧
引用通常以 Ref
作为后缀,以明确表示它们是特殊的对象:
this.inputRef = useRef('input');
11. 切换待办事项¶
现在,让我们添加一个新功能:将待办事项标记为已完成。这实际上比想象中要复杂得多。状态的所有者与显示它的组件并不相同。因此,TodoItem
组件需要向其父组件传达需要切换待办事项状态的信息。一种经典的做法是添加一个 回调属性 toggleState
。
在任务的 id 之前添加一个带有属性
type="checkbox"
的输入框,当状态isCompleted
为 true 时该复选框必须被勾选。小技巧
Owl 在其值为假值时不会使用
t-att
指令创建属性。向
TodoItem
添加回调属性toggleState
。在
TodoItem
组件的输入框上添加一个change
事件处理程序,并确保它使用待办事项 ID 调用toggleState
函数。让它运行起来!

12. 删除待办事项¶
最后一步是让用户能够删除一个待办事项。
在
TodoItem
中添加一个新的回调属性removeTodo
。在
TodoItem
组件的模板中插入<span class="fa fa-remove"/>
。每当用户点击它时,应该调用
removeTodo
方法。让它运行起来!
小技巧
如果你使用数组来存储待办事项列表,可以使用 JavaScript 的
splice
函数从数组中移除一个待办事项。
// find the index of the element to delete
const index = list.findIndex((elem) => elem.id === elemId);
if (index >= 0) {
// remove the element at index from list
list.splice(index, 1);
}

13. 带插槽的通用 Card
¶
在 之前的练习 中,我们构建了一个简单的 Card
组件。但说实话,它相当有限。如果我们想在卡片中显示一些任意内容,比如一个子组件,该怎么办呢?目前不行,因为卡片的内容是通过字符串描述的。不过,如果我们能将内容描述为一段模板,那将会非常方便。
这正是 Owl 的 slot 系统所设计用于的:允许编写通用组件。
让我们修改 Card
组件以使用插槽:
移除
content
属性。使用默认插槽来定义正文。
插入一些带有任意内容的卡片,例如一个
Counter
组件。(附加)添加属性验证。

14. 最小化卡片内容¶
最后,让我们为 Card
组件添加一个功能,使其更加有趣:我们希望有一个按钮来切换其内容(显示或隐藏)。
为
Card
组件添加一个状态,用于跟踪它是否处于打开状态(默认状态)或未打开状态。在模板中添加
t-if
以根据条件渲染内容在页眉中添加一个按钮,并修改代码以在点击按钮时切换状态
