OWL教程10 杂七杂八
一 Environment 环境
原文地址:https://github.com/odoo/owl/blob/master/doc/reference/environment.md
1 概述
环境是组件树中所有组件的共享对象。Owl本身并不使用它,但是对于应用程序开发人员来说,它可以在组件之间提供一个简单的通信通道(除了props之外)。
给应用程序的env作为组件属性分配给各个组件
Root
/ \
A B
同样,在应用程序启动时,env对象被冻结。这样做是为了确保对运行时发生的事情有一个更简单的逻辑模型。请注意,它只是浅层冻结,因此可以修改子对象。
2 设置环境
自定义环境的正确方法是在创建环境时将其提供给应用程序。
const env = {
_t: myTranslateFunction,
user: {...},
services: {
...
},
};
new App(Root, { env }).mount(document.body);
// or alternatively
mount(App, document.body, { env });
3 使用子环境
从特定组件及其子组件的角度来看,向环境添加一个(或多个)特定键有时是有用的。在这种情况下,上面提供的解决方案将不起作用,因为它设置了全局环境。
这种情况有两个钩子:useSubEnv和useChildSubEnv。
class SomeComponent extends Component {
setup() {
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
}
}
4 环境的内容
环境对象的内容完全由应用程序开发人员决定。然而,环境中附加键的一些很好的用例是:
- 一些配置键,
- 会话信息
- 通用服务(例如rpc服务)。
- 想要注入的其他实用函数,例如转换函数
这样做意味着组件很容易测试:我们可以简单地用模拟服务创建一个测试环境。
二 表单输入绑定
从html输入框或者textarea,select中读取值是很常见的需求, 完了实现这需求,一种可能的办法是手工来完成
class Form extends owl.Component {
state = useState({ text: "" });
_updateInputValue(event) {
this.state.text = event.target.value;
}
}
<div>
<input t-on-input="_updateInputValue" />
<span t-esc="state.text" />
</div>
这个当然工作。然而,这需要一些管道代码。此外,如果您需要与复选框、单选按钮或选择标记进行交互,则管道代码也会略有不同
为了帮助解决这种情况,Owl有一个内置的指令t-model:它的值应该是组件中观察到的值(通常是state.someValue)。使用t-model指令,我们可以编写更短的代码,等效于前面的示例:
class Form extends owl.Component {
state = { text: "" };
}
<div>
<input t-model="state.text" />
<span t-esc="state.text" />
</div>
t-model指令适用于、、、和:
<div>
<div>Text in an input: <input t-model="state.someVal"/></div>
<div>Textarea: <textarea t-model="state.otherVal"/></div>
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
<div>Selection:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Selection with radio buttons:
<span>
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
<label for="red">Red</label>
</span>
<span>
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
<label for="blue">Blue</label>
</span>
</div>
</div>
和事件处理一样,t-model指令接受以下修饰符:
Modifier | Description |
---|---|
.lazy | 发生change事件时才更新值 (default is on input event) |
.number | 尝试解析成一个数字 (using parseFloat ) |
.trim | trim the resulting value |
For example:
<input t-model.lazy="state.someVal" />
这些修饰符可以组合使用。例如,t-model.lazy。每当更改完成时,Number将只更新一个号码。
注意:在线的playground 有一个例子来展示它是如何工作的。
三 事件处理
1 事件处理
在组件模板中,能够将DOM元素上的处理程序注册到某些特定事件是很有用的。这就是模板的生命力所在。这是用t-on指令完成的。例如:
<button t-on-click="someMethod">Do something</button>
这将在javascript中大致翻译如下:
button.addEventListener("click", component.someMethod.bind(component));
后缀(在本例中为click)只是实际DOM事件的名称。t-on表达式的值应该是一个有效的javascript表达式,在当前组件的上下文中计算为一个函数。因此,可以获得对事件的引用,或者传递一些额外的参数。例如,以下所有表达式都是有效的:
<button t-on-click="someMethod">Do something</button>
<button t-on-click="() => this.increment(3)">Add 3</button>
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
注意在lambda函数中使用this关键字:这是在lambda函数中调用组件方法的正确方式
可以用下面的表达:
<button t-on-click="() => increment(3)">Add 3</button>
但是,increment可能会被解除绑定(除非组件在其setup函数中绑定了它)。
2 修改器
为了从事件处理程序中删除DOM事件细节(比如对event. preventdefault的调用),让它们专注于数据逻辑,可以将修饰符3 合成事件
Modifier | Description |
---|---|
.stop | 在调用方法之前调用event.stopPropagation() |
.prevent | 在调用方法之前调用event.preventDefault() |
.self | calls the method only if the event.target is the element itself |
`.capture | 在 capture 模式下绑定事件处理程序。 |
.synthetic | 定义一个合成事件处理程序(见下文) |
<button t-on-click.stop="someMethod">Do something</button>
请注意,修饰符可以组合使用(例如:t-on-click.stop.prevent),并且顺序可能很重要。例如t-on-click.prevent.Self将阻止所有点击,而t-on-click.self.Prevent只会阻止点击元素本身。
最后,可以容忍空处理程序,因为它们只能定义为应用修饰符。例如,
<button t-on-click.stop="">Do something</button>
这将简单地停止事件的冒泡。
3 合成事件
在某些情况下,为大型列表的每个元素附加一个事件处理程序会带来不小的开销。Owl提供了一种有效提高性能的方法:使用合成事件,它实际上只在文档主体上添加一个处理程序,并且会像预期的那样正确地调用该处理程序。
与常规事件的唯一区别是,事件是在文档主体处捕获的,因此在它实际到达文档主体之前不能停止它。因为在某些情况下可能会令人惊讶,所以默认情况下它是不启用的。
要启用它,只需使用.synthetic后缀:
<div>
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
<button t-on-click.synthetic="doSomething" ...>
<!-- some content -->
</button>
</t>
</div>
4 On Component
t-on指令也适用于子组件:
<div>
in some template
<Child t-on-click="dosomething"/>
</div>
这将捕获子组件中包含的任何html元素上的所有点击事件。注意,如果子组件减少为一个(或多个)文本节点,则单击它将不会调用处理程序,因为该事件将由浏览器在父元素(本例中为div)上调度。
四 错误处理
1 概述
默认情况下,只要在渲染Owl应用程序时出现错误,我们就会销毁整个应用程序。否则,我们无法对结果组件树的状态提供任何保证。它可能是无可救药的损坏,但没有任何用户可见的反馈。
显然,破坏应用程序通常有点极端。这就是为什么我们需要一种机制来处理渲染错误(以及来自生命周期钩子的错误):onError钩子。
主要思想是onError钩子注册一个函数,该函数将在错误发生时被调用。该函数需要处理这种情况,大多数情况下是通过更新一些状态并重新渲染自身,以便应用程序可以返回到正常状态。
2 管理错误
无论何时使用onError生命周期钩子,所有来自子组件渲染和/或生命周期方法调用的错误都将被捕获并传递给onError方法。这允许我们正确地处理错误,而不会破坏应用程序。
有一些重要的事情需要知道:
- 如果在内部渲染周期中发生的错误没有被捕获,那么Owl将销毁整个应用程序。这样做是有目的的,因为Owl不能保证从此时开始状态没有被损坏。
- 来自事件处理程序的错误不受onError或任何其他owl机制的管理。这取决于应用程序开发人员如何正确地从错误中恢复
- 如果错误处理程序无法正确处理错误,它可以重新抛出错误,然后Owl将尝试在组件树中查找另一个错误处理程序。
3 案例
例如,下面是我们如何实现一个通用组件ErrorBoundary来渲染它的内容,以及在发生错误时的回退。
class ErrorBoundary extends Component {
static template = xml`
<t t-if="error" t-slot="fallback">An error occurred</t>
<t t-else="" t-slot="content"`;
setup() {
this.state = useState({ error: false });
onError(() => (this.state.error = true));
}
}
使用ErrorBoundary很简单:
<ErrorBoundary>
<SomeOtherComponent/>
<t t-set-slot="fallback">Some specific error message</t>
</ErrorBoundary>
注意,我们在这里需要小心:回退UI不应该抛出任何错误,否则我们可能会陷入无限循环(另外,请参阅关于t-slot指令的更多信息的插槽页面)。
五 props验证
原文地址: https://github.com/odoo/owl/blob/master/doc/reference/props.md
概述:
在Owl中,props(properties的缩写)是父组件传递给子组件所有数据的对象。
class Child extends Component {
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
}
class Parent extends Component {
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
static components = { Child };
state = useState({ a: "fromparent" });
}
在这个例子中,子组件从父对象接收到两个属性,a和b,他们被包含在props对象中,每个对象都在父组件的上下文中赋值,所以, prop.a=“fromparent”, props.b=‘string’;
注意,props是一个只有从子组件才有意义的对象。
定义
props对象在模板中定义所有的属性,下列情况除外:
- 以t-开头的属性除外(他们是QWeb指令)
在下面的例子中:
<div>
<ComponentA a="state.a" b="'string'"/>
<ComponentB t-if="state.flag" model="model"/>
</div>
props对象包含下列key:
- for
ComponentA
:a
andb
, - for
ComponentB
:model
,
属性对比
每当Owl在模板中遇到子组件时,它都会对所有属性进行粗略的比较。如果它们都是引用相等的,那么子组件甚至不会被更新。否则,如果至少有一个属性发生了变化,那么Owl将更新它。
然而,有些情况,我们知道两个值不同,但是他们有相同的效果,不应该被Owl认为不同。 例如模板中的匿名函数总是不同的,但是他们大多数不应该看做不同。
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete="() => deleteTodo(todo.id)" />
</t>
这种情况,我们可以使用.alike后缀
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<Todo todo="todo" onDelete.alike="() => deleteTodo(todo.id)" />
</t>
这告诉Owl,指定的属性应该被看做是相等的(换句话说,他们应该从属性对比的列表中移走)
注意虽然大多数匿名函数都应该用alike,但并不是所有情况都是这样。 它依赖于匿名函数捕获了什么值,下面的例子展示了用.alike是错误的
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<!-- Probably wrong! todo.isCompleted may change -->
<Todo todo="todo" toggle.alike="() => toggleTodo(todo.isCompleted)" />
</t>
总结一波:
- 如果捕获的值不会改变那么就用alike
- 如果捕获的值可能会改变,就不用alike
绑定函数属性
It is common to have the need to pass a callback as a prop. Since Owl components are class based, the callback frequently needs to be bound to its owner component. So, one can do this:
传递一个回调函数作为属性是很普遍的,因为Owl组件是基于类的,回调函数经常需要绑定到它自身,所以,可以这样做:
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback="doSomething"/>
</div>`;
setup() {
this.doSomething = this.doSomething.bind(this);
}
doSomething() {
// ...
}
}
However, this is such a common use case that Owl provides a special suffix to do just that: .bind
. This looks like this:
然而,这是一个通用做法,Owl提供了一个特殊的后缀“.bind”,看上去像这样
class SomeComponent extends Component {
static template = xml`
<div>
<Child callback.bind="doSomething"/>
</div>`;
doSomething() {
// ...
}
}
.bind 后缀实现了.alike, 所以这些属性不会引发额外的渲染。
动态属性
The t-props
directive can be used to specify totally dynamic props:
t-props指令可以用来指定动态指令:
<div t-name="ParentComponent">
<Child t-props="some.obj"/>
</div>
class ParentComponent {
static components = { Child };
some = { obj: { a: 1, b: 2 } };
}
默认属性
如果static defaultProps
属性被定义了,它将用来完成属性的props的设置,如果父组件忘记传的话。
class Counter extends owl.Component {
static defaultProps = {
initialValue: 0,
};
...
}
上面的例子中,initialValue 属性的默认值是0
属性验证
当一个应用变的复杂的时候,非正式的定义属性将变的不安全,这将导致两个问题:
- 很难知道一个组件怎么用,除非看它的代码
- 不安全,容易发送错误的数据,当重构一个组件或者它的父组件的时候
一个组件类型系统解决了这两个问题,通过描述属性的类型和形式,下面描述了它在Owl中是如何工作的:
- props key是一个静态的key,不跟this.props不同,在组件实例中
- 它是可选的,在组件中不定义props也是可以的
- 属性将被验证,无论在这个组件新建或者更新的时候
- 属性只有在开发模型下被验证
- 如果key跟描述不符合,将抛出一个异常
- 如果一个验证的key在props被定义, 父组件传递的其他非定义的key会引发一个错误。
- 他是一个对象或者字符串列表
- 字符串列表是属性定义的简化写法,只列出了属性的名字,如果名字以?结尾,这表示是可选的
- 所有的属性都是必须的,除非他们被定义成optional: true
- 验证类型包括: Number, String, Boolean, Object, Array, Date, Function,以及所有的构造函数,(如果你有个Person class,它可以被看成换一个类型)
- 数组的同构的,(所有的元素类型都相同)
对于每个key,prop定义可能是boolean, a constructor, a list of constructors, or an object:
- a boolean: 暗示这个属性一定要存在,而且是强制性的。
- a constructor: 这将描述数据类型,例如 id: Number
描述 属性
id` 是一个数字 - 一个对象描述了一个值作为对象,这通过value来实现,例如{value: false}指定相应的value应该等于false。
- 一个构造方法的列表,这表示允许多个类型,id: [Number, String] ,表示id可以是数字也可以是字符串。
- 一个对象,这可以让定义更加全面,可以包含下面的子keys,(不是强制的)
type
: 数据类型element
: 如果类型是数组,那么element验证数组中元素的类型,如果没有设置这个属性,我们只验证数组shape
: 如果type是一个对象,shape 可以 描述了对象的接口(对象的属性),如果没设置,我们只验证对象values
: 如果类型是一个对象,values 描述了对象的对象中的接口,这允许验证对象通过映射的方式,validate
: 这是一个函数,返回一个布尔值决定该值是否通过验证,用来添加自定义的逻辑optional
: 如果为真,属性不是强制的。
还有一个特殊*属性, 这意味着额外的属性是允许的,这有时候用来让组件将他们的属性传播给他们的子组件。
注意,默认值不能定义为强制性的,这会引发一个验证错误。
Examples:
class ComponentA extends owl.Component {
static props = ['id', 'url'];
...
}
class ComponentB extends owl.Component {
static props = {
count: {type: Number},
messages: {
type: Array,
element: {type: Object, shape: {id: Boolean, text: String }
},
date: Date,
combinedVal: [Number, Boolean],
optionalProp: { type: Number, optional: true }
};
...
}
// only the existence of those 3 keys is documented
static props = ['message', 'id', 'date'];
// only the existence of those 3 keys is documented. any other key is allowed.
static props = ['message', 'id', 'date', '*'];
// size is optional
static props = ['message', 'size?'];
static props = {
messageIds: {type: Array, element: Number}, // list of number
otherArr: {type: Array}, // just array. no validation is made on sub elements
otherArr2: Array, // same as otherArr
someObj: {type: Object}, // just an object, no internal validation
someObj2: {
type: Object,
shape: {
id: Number,
name: {type: String, optional: true},
url: String
]}, // object, with keys id (number), name (string, optional) and url (string)
someObj3: {
type: Object,
values: { type: Array, element: String },
}, // object with arbitary keys where values are arrays of strings
someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop
kindofsmallnumber: {
type: Number,
validate: n => (0 <= n && n <= 10)
},
size: {
validate: e => ["small", "medium", "large"].includes(e)
},
someId: [Number, {value: false}], // either a number or false
};
注意: 验证代码是通过validate utility function. 调用的,这是owl框架自动调用的
好的实践
一个props对象是从父组件传递的值的集合。 所以,父组件拥有他们,子组件永远不要修改他们。
class MyComponent extends Component {
constructor(parent, props) {
super(parent, props);
props.a.b = 43; // Never do that!!!
}
}
属性应该被认为在子组件中是只读的,如果需要修改他们,请改请求父组件来做这件事,(比如通过事件)
props可以包含任何值, 字符串、对象、类、甚至是回调函数可以给子组件,(但是,相比较回调函数来说,events看起来更合适)
六 并发模式
参考地址: https://github.com/odoo/owl/blob/master/doc/reference/concurrency_model.md
1 概述
Owl从一开始就是用异步组件设计的。这来自于willStart和willUpdateProps生命周期钩子。有了这些异步钩子,就可以构建复杂的高度并发应用程序。
Owl并发模式有几个好处:它可以延迟呈现,直到一些异步操作完成;它可以延迟加载库,同时保持前一个屏幕的完整功能。它也有很好的性能原因:Owl使用它在一个动画帧中只应用一次许多不同渲染的结果。Owl可以取消不再相关的渲染,重新启动它,在某些情况下重用它。
但是,尽管使用并发非常简单(并且是默认行为),但异步很难,因为它引入了一个额外的维度,极大地增加了应用程序的复杂性。本节将解释Owl如何管理这种复杂性,以及并发渲染在一般情况下是如何工作的
2 渲染组件
渲染这个词有点模糊,因此,让我们更精确地解释一下在屏幕上显示Owl组件的过程。
当挂载或更新组件时,将启动一个新的渲染。它有两个阶段:虚拟渲染和修补。
3 虚拟渲染
此阶段表示在内存中渲染模板的过程,该过程创建所需组件的虚拟表示(html)。这个阶段的输出是一个虚拟DOM。
它是异步的:每个子组件要么需要创建(因此,需要调用willStart),要么需要更新(使用willUpdateProps方法完成)。这完全是一个递归过程:组件是组件树的根,每个子组件都需要(虚拟地)渲染。
4 修补
一旦渲染完成,它将应用于下一个动画帧。这是同步完成的:整个组件树被修补为真正的DOM。
5 语法
我们在这里非正式地描述了在应用程序中创建/更新组件的方式。在这里,有序列表描述按顺序执行的操作,项目列表描述并行执行的操作。
场景1: 假设我们想要渲染下面的组件树:
A
/ \
B C
/ \
D E
下面是我们挂载根组件时发生的情况(使用类似app.mount(document.body)的代码)
1 willStart is called on A
2 when it is done, template A is rendered.
component B is created
willStart is called on B
template B is rendered
component C is created
willStart is called on C
template C is rendered
component D is created
willStart is called on D
template D is rendered
component E is created
willStart is called on E
template E is rendered
3 每个组件按以下顺序被修补到一个独立的DOM元素中:E、D、C、B、a(因此实际的完整DOM树是一次创建的)
4 组件根元素实际上被附加到document.body中
5 按照以下顺序在所有组件上递归地调用所挂载的方法:E, D, C, B, A。
场景2 : 更新一个组件
现在,让我们假设用户点击了C中的某个按钮,这会导致状态更新,它应该是:
- update
D
, - remove
E
, - add new component
F
.
所以,组件树应该是这样的:
A
/ \
B C
/ \
D F
下面是Owl要做的:
1 由于状态改变,在C上调用render方法
2 组件C被重新渲染
组件D被更新
a. D 调用willUpdateProps(异步)
b. D的模板被渲染
组件F被创建
a.F的willstart被调用(异步)
b.F的模板被渲染
3 在组件C、D上递归调用willPatch钩子(在组件F上不调用,因为它还没有挂载)
4 组件F, D按此顺序进行修补
5 组件C被打补丁,这将递归地导致:
在E上调用willUnmout钩子
销毁E
标记是非常小的助手,它使编写内联模板变得容易。目前只有一个可用的标记:xml。
6 异步渲染
使用异步代码总是会给系统增加很多复杂性。每当系统的不同部分同时处于活动状态时,就需要仔细考虑所有可能的交互。显然,对于Owl组件也是如此。
Owl异步渲染模型有两个不同的常见问题:
- 任何组件都可以延迟整个应用程序的渲染(初始和后续)
- 对于给定的组件,有两种独立的情况会触发异步重新渲染:状态的更改或props的更改。这些更改可能在不同的时间进行,而Owl无法知道如何协调最终的渲染结果。
以下是一些关于如何使用异步组件的提示:
- 尽量减少异步组件的使用!
- 延迟加载外部库是异步渲染的一个很好的用例。这基本上没问题,因为我们可以假设它只需要几分之一秒,而且只需要一次。
七 Portal
有时候,能够在组件的边界之外呈现一些内容是很有用的。为了做到这一点,Owl提供了一个特殊的指令:
class SomeComponent extends Component {
static template = xml`
<div>this is inside the component</div>
<div t-portal="'body'">and this is outside</div>
`;
}
t-portal指令接受一个有效的css选择器作为参数。将在相应的位置安装已装载模板的内容。注意,Owl需要在搬运内容的位置插入一个空文本节点。
八 预编译模板
fatux: 预编译模板, 可能一些老的浏览器不支持才需要这么做吧…
Owl被设计为由Odoo javascript框架使用。由于Odoo以自己的非标准方式处理其资产,因此决定/假设Owl将在运行时编译模板。
然而,在某些情况下,它不是最优的,甚至更糟,不可能这样做。例如,浏览器扩展不允许javascript代码创建一个新函数(使用新的function(…)语法)。
因此,在这些情况下,需要提前编译模板。在Owl中可以做到这一点,但是工具仍然很粗糙。目前,流程如下:
- 在XML文件中编写模板(使用t-name指令来声明模板的名称)
- 将它们编译到templates.js文件中
- 获取owl. life .runtime.js文件(这是一个没有编译器的owl构建文件)
- 将owl. life .runtime.js和template.js与你的资产捆绑在一起(owl需要放在模板之前)
下面是关于如何将xml文件编译成js文件的更详细的解释:
- 在本地克隆owl存储库
- NPM install安装所有必需的工具
- NPM运行build:runtime构建owl. life .runtime.js文件
- NPM运行build:compiler来构建模板编译器
- NPM运行compile_templates——path/to/your/templates会扫描你的目标文件夹,找到所有的XML文件,得到所有的模板,编译它们,并生成一个templates.js文件。