说明:这是对Odoo官方Owl公开的资料的翻译。以后再添加我在Odoo的Owl源码中领悟到的技术说明
这篇文章的确很烧脑,但是也只能从这里先学习,以后可以回过头来再看看这篇文章,会觉得很清晰。
建议先学习typescript和Qweb的知识
对于本教程,我们将构建一个非常简单的待办事项列表应用程序。 该应用程序应满足以下要求:
这个项目将是发现和学习一些重要的Owl概念的机会,例如组件,存储以及如何组织应用程序。
对于本教程,我们将做一个非常简单的项目,其中包含静态文件,并且没有其他工具。 第一步是创建以下文件结构:
该应用程序的入口点是文件index.html,该文件应具有以下内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
<script src="owl.js"></script>
<script src="app.js"></script>
</head>
<body></body>
</html>
然后,可以暂时将app.css留空。 以后对我们的应用程序进行样式设置将很有用。 app.js是我们编写所有代码的地方。 现在,我们只需要输入以下代码:
(function () {
console.log("hello owl", owl.__info__.version);
})();
请注意,我们将所有内容放入立即执行的函数中,以避免将任何内容泄漏到全局范围。
最后,owl.js应该是从Owl存储库下载的最新版本(如果愿意,可以使用owl.min.js)。
现在,该项目应该已经准备好了。 将index.html文件加载到浏览器中时,应显示一个空白页面,标题为Owl Todo App,并且应在控制台中记录一条消息,例如hello owl 1.0.0。
Owl应用程序由组件组成(
https://github.com/odoo/owl/blob/master/doc/reference/component.md),具有单个根组件。 让我们首先定义一个App组件。 通过以下代码替换app.js中函数的内容:
const { Component } = owl;
const { xml } = owl.tags;
const { whenReady } = owl.utils;
// Owl Components
class App extends Component {
static template = xml`<div>todo app</div>`;
}
// Setup code
function setup() {
const app = new App();
app.mount(document.body);
}
whenReady(setup);
现在,在浏览器中重新加载页面应该显示一条消息。
代码非常简单,但是让我们更详细地解释最后一行。浏览器尝试尽快执行app.js中的javascript代码,并且当我们尝试安装App组件时,可能发生DOM尚未准备就绪的情况。为了避免这种情况,我们使用whenReady帮助程序将setup函数的执行延迟到DOM准备就绪为止。
注意1:在更大的项目中,我们将代码分成多个文件,组件位于子文件夹中,而主文件则将初始化应用程序。但是,这是一个很小的项目,我们希望使其尽可能简单。
注意2:本教程使用静态类字段语法。并非所有浏览器都支持此功能。大多数实际项目都会转换其代码,因此这不是问题,但是对于本教程而言,如果您需要在每个浏览器上都能使用的代码,则需要将每个static关键字转换为对该类的分配:
class App extends Component {}
App.template = xml`<div>todo app</div>`;
注意3:使用xml helper(
https://github.com/odoo/owl/blob/master/doc/reference/tags.md#xml-tag)编写内联模板是不错的选择,但是没有突出显示语法,这使得格式错误的xml非常容易。 一些编辑器在这种情况下支持语法突出显示。 例如,VS Code具有附加Comment tagged template,如果已安装,它将正确显示标签模板:
static template = xml /* xml */`<div>todo app</div>`;
注4:大型应用程序可能希望能够翻译模板。 使用内联模板会使它稍微困难一些,因为我们需要其他工具来从代码中提取xml,并将其替换为转换后的值。
现在已经完成了基础工作,是时候开始考虑任务了。 为了完成我们需要的工作,我们将使用以下键将任务作为对象数组来跟踪:
现在,我们决定了状态的内部格式,让我们向App组件添加一些演示数据和模板:
该模板包含一个t-foreach循环以迭代任务。 因为组件是渲染上下文,所以它可以从组件中找到tasks列表。 请注意,我们将每个任务的id用作t-key,这很常见。 有两个CSS类:task-list和task,我们将在下一节中使用它们。
最后,请注意t-att-checked属性的使用:通过t-att为属性添加前缀可以使其具有动态性。 Owl将评估表达式并将其设置为属性的值。
到目前为止,我们的任务列表看起来很糟糕。 让我们将以下内容添加到app.css中:
.task-list {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.task {
font-size: 18px;
color: #111111;
}
这个更好。 现在,让我们添加一个额外的功能:完成的任务的样式应略有不同,以使它们变得不那么重要。 为此,我们将在每个任务上添加一个动态CSS类:
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">
.task.done {
opacity: 0.7;
}
注意,这里我们还使用了动态属性。
现在很明显,应该有一个Task组件来封装任务的外观和行为。
这个Task组件将显示一个任务,但是不能拥有该任务的状态:一条数据应该只有一个所有者。 否则会带来麻烦。 因此,Task组件将获得其数据作为prop。 这意味着数据仍归App组件所有,但可以由Task组件使用(无需修改)。
由于我们在移动代码,因此是重构代码的好机会:
这里发生了很多事情:
我们仍然使用硬编码任务列表。现在是时候让用户自己添加任务了。第一步是将输入添加到App组件。但是此输入将不在任务列表之内,因此我们需要调整App模板,js和CSS:
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
<div class="task-list">
<t t-foreach="tasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
</div>
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
现在,我们有了一个有效的输入,只要用户添加了任务,该输入就会登录到控制台。 请注意,当您加载页面时,输入未聚焦。 但是添加任务是任务列表的核心功能,因此让我们通过集中输入使其尽可能快。
由于App是组件,因此它具有可实现的mounted lifecycle method(
https://github.com/odoo/owl/blob/master/doc/reference/component.md#lifecycle)。 我们还需要通过使用带有useRef钩子的t-ref指令来获得对输入的引用:
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
// on top of file:
const { useRef } = owl.hooks;
// in App
inputRef = useRef("add-input");
mounted() {
this.inputRef.el.focus();
}
inputRef被定义为类字段,因此等效于在构造函数中对其进行定义。 它仅指示Owl使用相应的t-ref关键字保留对任何内容的引用。 然后,我们实现mounted的生命周期方法,现在我们有了一个活动引用,可以用来集中输入。
在上一节中,我们完成了所有工作,除了实现了实际创建任务的代码! 所以,让我们现在开始。
我们需要一种生成唯一id号的方法。 为此,我们只需在App中添加一个nextId号。 同时,让我们在App中删除演示任务:
nextId = 1;
tasks = [];
现在,可以实现addTask方法:
addTask(ev) {
// 13 is keycode for ENTER
if (ev.keyCode === 13) {
const title = ev.target.value.trim();
ev.target.value = "";
if (title) {
const newTask = {
id: this.nextId++,
title: title,
isCompleted: false,
};
this.tasks.push(newTask);
}
}
}
这几乎可以用,但是如果您对其进行测试,则会发现当用户按下Enter键时,不会显示任何新任务。 但是,如果添加debugger或console.log语句,您将看到代码实际上按预期运行。 问题在于,Owl无法知道它是否需要重新呈现用户界面。 我们可以通过使用useState挂钩使tasks具有反应性来解决此问题:
// on top of the file
const { useRef, useState } = owl.hooks;
// replace the task definition in App with the following:
tasks = useState([]);
现在可以正常工作了!
如果您尝试将任务标记为已完成,则可能已经注意到文本的不透明度没有改变。 这是因为没有代码可以修改isCompleted标志。
现在,这是一个有趣的情况:任务由Task组件显示,但它不是其状态的所有者,因此无法修改它。 相反,我们希望传达将任务切换到App组件的请求。 由于App是Task的父级,因此我们可以在Task中触发 trigger (
https://github.com/odoo/owl/blob/master/doc/reference/event_handling.md)事件并在App中监听。
在Task中,将input更改为:
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
并添加toggleTask方法:
toggleTask() {
this.trigger('toggle-task', {id: this.props.task.id});
}
现在,我们需要在App模板中监听该事件:
<div class="task-list" t-on-toggle-task="toggleTask">
并实现toggleTask代码:
toggleTask(ev) {
const task = this.tasks.find(t => t.id === ev.detail.id);
task.isCompleted = !task.isCompleted;
}
现在让我们添加执行删除任务的可能性。 为此,我们首先需要在每个任务上添加一个废纸icon图标,然后像上一节中一样继续进行。
首先,让我们更新Task模板,css和js:
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
<span><t t-esc="props.task.title"/></span>
<span class="delete" t-on-click="deleteTask"></span>
</div>
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
deleteTask() {
this.trigger('delete-task', {id: this.props.task.id});
}
现在,我们需要监听App中的delete-task事件:
<div class="task-list" t-on-toggle-task="toggleTask" t-on-delete-task="deleteTask">
deleteTask(ev) {
const index = this.tasks.findIndex(t => t.id === ev.detail.id);
this.tasks.splice(index, 1);
}
看一下代码,很明显,我们现在有了处理分散在多个地方的任务的代码。 而且,它混合了UI代码和业务逻辑代码。 Owl有一种与用户界面分开管理状态的方法:Store(
https://github.com/odoo/owl/blob/master/doc/reference/store.md)。
让我们在应用程序中使用它。 (对于我们的应用程序而言)这是一个相当大的重构,因为它涉及从组件中提取所有与任务相关的代码。 这是app.js文件的新内容:
现在,我们的TodoApp可以很好地运行,除非用户关闭或刷新浏览器! 仅将应用程序的状态保存在内存中确实很不方便。 为了解决这个问题,我们将任务保存在本地存储中。 使用我们当前的代码库,这是一个简单的更改:仅需要更新设置代码。
关键是要使用这样的事实,即商店是一个EventBus(
https://github.com/odoo/owl/blob/master/doc/reference/event_bus.md),它在每次更新时都会触发一个update事件。
我们差不多完成了,我们可以添加/更新/删除任务。 唯一缺少的功能是可以根据任务的完成状态显示任务。 我们将需要跟踪App中过滤器的状态,然后根据其值过滤可见任务。
// on top of file, readd useState:
const { useRef, useDispatch, useState, useStore } = owl.hooks;
// in App:
filter = useState({value: "all"})
get displayedTasks() {
switch (this.filter.value) {
case "active": return this.tasks.filter(t => !t.isCompleted);
case "completed": return this.tasks.filter(t => t.isCompleted);
case "all": return this.tasks;
}
}
setFilter(filter) {
this.filter.value = filter;
}
最后,我们需要显示可见的过滤器。 我们可以做到这一点,同时在主列表下方的小面板中显示任务数:
<div class="todo-app">
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
<div class="task-list">
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
<Task task="task"/>
</t>
</div>
<div class="task-panel" t-if="tasks.length">
<div class="task-counter">
<t t-esc="displayedTasks.length"/>
<t t-if="displayedTasks.length lt tasks.length">
/ <t t-esc="tasks.length"/>
</t>
task(s)
</div>
<div>
<span t-foreach="['all', 'active', 'completed']"
t-as="f" t-key="f"
t-att-class="{active: filter.value===f}"
t-on-click="setFilter(f)"
t-esc="f"/>
</div>
</div>
</div>
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}
请注意,这里我们使用对象语法动态设置了过滤器的类:每个键都是要设置为真的类。
我们的清单功能齐全。 我们仍然可以添加一些额外的细节来改善用户体验。
.task:hover {
background-color: #def0ff;
}
<input type="checkbox" t-att-checked="props.task.isCompleted"
t-att-id="props.task.id"
t-on-click="dispatch('toggleTask', props.task.id)"/>
<label t-att-for="props.task.id"><t t-esc="props.task.title"/></label>
.task.done label {
text-decoration: line-through;
}
现在我们的申请已经完成。 它可以正常工作,UI代码与业务逻辑代码完全隔离,可以测试,所有代码都在150行以下(包括模板!)。
供参考,这是最终代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
<script src="owl.js"></script>
<script src="app.js"></script>
</head>
<body></body>
</html>
.todo-app {
width: 300px;
margin: 50px auto;
background: aliceblue;
padding: 10px;
}
.todo-app > input {
display: block;
margin: auto;
}
.task-list {
margin-top: 8px;
}
.task {
font-size: 18px;
color: #111111;
display: grid;
grid-template-columns: 30px auto 30px;
}
.task:hover {
background-color: #def0ff;
}
.task > input {
margin: auto;
}
.delete {
opacity: 0;
cursor: pointer;
text-align: center;
}
.task:hover .delete {
opacity: 1;
}
.task.done {
opacity: 0.7;
}
.task.done label {
text-decoration: line-through;
}
.task-panel {
color: #0088ff;
margin-top: 8px;
font-size: 14px;
display: flex;
}
.task-panel .task-counter {
flex-grow: 1;
}
.task-panel span {
padding: 5px;
cursor: pointer;
}
.task-panel span.active {
font-weight: bold;
}