OWL教程10 杂七杂八

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指令接受以下修饰符:

ModifierDescription
.lazy发生change事件时才更新值 (default is on input event)
.number尝试解析成一个数字 (using parseFloat)
.trimtrim 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 合成事件

ModifierDescription
.stop在调用方法之前调用event.stopPropagation()
.prevent在调用方法之前调用event.preventDefault()
.selfcalls the method only if the event.target is the element itself
`.capturecapture 模式下绑定事件处理程序。
.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 and b,
  • 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无法知道如何协调最终的渲染结果。

以下是一些关于如何使用异步组件的提示:

  1. 尽量减少异步组件的使用!
  2. 延迟加载外部库是异步渲染的一个很好的用例。这基本上没问题,因为我们可以假设它只需要几分之一秒,而且只需要一次。

七 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中可以做到这一点,但是工具仍然很粗糙。目前,流程如下:

  1. 在XML文件中编写模板(使用t-name指令来声明模板的名称)
  2. 将它们编译到templates.js文件中
  3. 获取owl. life .runtime.js文件(这是一个没有编译器的owl构建文件)
  4. 将owl. life .runtime.js和template.js与你的资产捆绑在一起(owl需要放在模板之前)

下面是关于如何将xml文件编译成js文件的更详细的解释:

  1. 在本地克隆owl存储库
  2. NPM install安装所有必需的工具
  3. NPM运行build:runtime构建owl. life .runtime.js文件
  4. NPM运行build:compiler来构建模板编译器
  5. NPM运行compile_templates——path/to/your/templates会扫描你的目标文件夹,找到所有的XML文件,得到所有的模板,编译它们,并生成一个templates.js文件。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值