OWL架构以及为啥要设计OWL
原文地址: https://github.com/odoo/owl/tree/master/doc/miscellaneous
将该目录下的四篇文章合成了一篇
一 Owl架构
我们解释Owl是怎么被设计的.
警告: 这些笔记本质上是技术性的,是为了用owl工作的人或者想理解它的设计理念的人准备的.
1 概览
粗略的讲,Owl有五个主要部分:
- 虚拟DOM系统(src/blockdom)
- 组件系统(src/component)
- 模板编译器(src/compiler 目录)
- 一个小的运行时让各层组合在一起(src/app)
- 响应式系统(src/reactivity.ts)
还有一些其他的文件,但是掌握了这五大部分就可以理解Owl的核心
虚拟dom是一种基于块的优化虚拟dom,支持多块(针对fragments)。owl渲染的所有内容都在内部由虚拟节点表示。虚拟dom的任务是有效地表示应用程序的当前状态,并在需要时构建实际的dom表示,或者在需要时更新dom。
渲染分为两个阶段:
- 虚拟渲染: 在内存中生成虚拟dom,异步的
- patch(补丁): 将虚拟dom刷新到屏幕(同步的)
渲染涉及几个类:
- Component
- scheduler
- fibers: 一个包含元数据的小的对象, 关联到被渲染的组件
组件通过动态的组件树来组织, 在用户界面中可见,当组件C的的渲染启动时:
- C创建一个fiber类,包含了props信息.
- C的虚拟渲染阶段开始(异步的渲染所有的子组件)
- fiber添加到调度器中, 如果fiber被完成,调度器在每个动画帧中持续的轮询
- 一旦渲染完成, 调度器将调用任务回调函数, 它会用来刷新dom(如果在此期间没有取消)
2 VDOM
Owl是一个声明式的组件系统, 我们声明了组件树的结构, Owl将它转换成一组命令式的操作.这个转换是通过虚拟dom完成的,这是owl的底层实现,大多数开发者不需要直接调用虚拟dom的方法.
虚拟dom背后的主要思想是保持dom的内存结构(称之为虚拟节点), 当发生一些变化时,重新生成一个新的内存结构,然后比较二者之间的差异,然后应用这些变化.
vdom 暴露了两个函数:
h : 创建一个新的虚拟节点
patch: 比较两个虚拟节点,然后应用两者之间的不同
注意: Owl’s的虚拟dom是 snabbdom(一个开源的虚拟dom js库)的分支.
二 跟vue,react比较
OWL,React和Vue具备相同的主要特性: 他们允许开发者创建声明式的用户接口,要做到这点, 这些框架都使用了虚拟dom,然而,他们依然有很多差异.
这里将重点介绍其中的一些差异.
1 size
OWL更小,工作在更底层,jquery不是相同类型的框架,但是比较起来也很有趣
Framework | Size (minified, gzipped) |
---|---|
OWL | 18kb |
Vue + VueX | 30kb |
Vue + VueX + Vue Router | 39kb |
React + ReactDOM + Redux | 40kb |
jQuery | 30kb |
这种比较并不是很公平, 因为我们没有比较他们提供的功能, VueX和Vue Router提供了更高级的用例.
fatux: 当前这个网速下,几十K的差距,真的重要吗?
2 基于类
React和Vue都不再使用类来定义组件。他们更喜欢功能性更强的方法,特别是带有新钩子机制的方法。
这有一些优点和缺点。但是最终的结果是React和Vue都提供了多种不同的方式来定义新组件。相反,Owl只有一种机制:基于类的组件。我们相信Owl组件对于我们所有的用例来说都足够快,使它对开发人员来说尽可能简单更有价值(对我们来说)。
此外,函数或基于类的组件不仅仅是语法。函数带有组合的思维方式,而类是关于继承的。显然,这两种机制都是重用代码的重要机制。同样,一个不排斥另一个。
显然,UI框架的世界正朝着组合的方向发展,这有很多很好的理由。Owl仍然擅长组合(例如,Owl支持槽,这是生成通用可重用组件的主要机制)。但是它也可以使用继承(这一点非常重要,因为模板也可以通过xpath转换继承)。
3 工具/构建步骤
OWL被设计成易于以独立的方式使用。由于各种原因,Odoo不希望依赖于标准的web工具(如webpack),而OWL可以通过简单地向页面添加脚本标签来使用。
<script src="owl.min.js" />
相比之下,React鼓励使用JSX,这需要一个构建步骤,而大多数Vue应用程序使用单文件组件,这也需要一个构建步骤。
另一方面,在某些情况下,外部工具可能会使其更难使用,但它也带来了很多好处。React/Vue都有一个庞大的生态系统。
请注意,由于Owl不依赖于任何外部工具或库,因此很容易集成到任何构建工具链中。此外,由于我们不能依赖于其他工具,我们做了很多努力来充分利用网络平台。
例如,Owl使用每个浏览器附带的标准xml解析器。因此,Owl不必编写自己的模板解析器。另一个例子是xml标记辅助函数,它利用本地模板字面量,允许以自然的方式直接在javascript代码中编写xml模板。这可以很容易地与编辑器插件集成,在模板内实现自动完成。
4 模板
OWL使用自己的QWeb引擎,该引擎在需要时在前端编译模板。这对于我们的用例来说非常方便,特别是因为模板是在XML文件中描述的,并且可以通过xpath进行修改。由于Odoo的核心是一个模块化应用程序,因此这对我们来说是一个重要的特性。
<div>
<button t-on-click="increment">Click Me! [<t t-esc="state.value"/>]</button>
</div>
Vue其实有点类似。它的模板语言有点接近QWeb,用t代替了v。但是,它的功能也更全面。例如,Vue模板有插槽或事件修饰符。一个很大的区别是,大多数Vue应用程序需要提前构建,以便将模板编译成javascript函数。请注意,Vue有一个单独的构建,其中包括模板编译器。
相比之下,大多数React应用程序不使用模板语言,而是编写一些JSX代码,这些代码通过构建步骤预编译为纯JavaScript。这个例子是用(有点过时的)React类系统完成的:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
这样做的优点是拥有Javascript的全部功能,但没有模板语言那么结构化。请注意,这个工具非常令人印象深刻:在github上有一个jsx的语法高亮显示!
通过比较,下面是等效的Owl组件,使用xml标记帮助器编写:
class Clock extends Component {
static template = xml`
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
`;
}
5 异步渲染
这实际上是OWL和React/Vue之间的一个很大的区别:OWL中的组件是完全异步的。它们的生命周期中有两个异步钩子:
willStart(在组件开始渲染之前)
willUpdateProps(在设置新props之前)
这两个方法都可以实现并返回一个promise。然后,渲染DOM之前等待这些promise完成。这对于某些用例是有用的:例如,组件可能希望在其willStart钩子中获取外部库(日历组件可能需要专门的日历呈现库)。
class MyCalendarComponent extends owl.Component {
...
willStart() {
return utils.lazyLoad('static/libs/fullcalendar/fullcalendar.js');
}
...
}
这可能是危险的(停止渲染等待网络请求完成),但它也非常强大,正如Odoo Web Client所展示的那样。
惰性加载静态库显然可以用React/Vue完成,但它更复杂。例如,在Vue中,您需要使用动态import关键字,该关键字需要在构建时进行编译,以便异步加载组件(请参阅文档)。
6 Reactivity
React有一个简单的模型:每当状态改变时,它就被一个新状态所取代(通过setState方法)。然后,更新DOM。这很简单、有效,但写起来有点别扭。
Vue有一点不同:它用getter /setter神奇地替换了状态中的属性。这样,当组件读取的状态发生变化时,它就可以通知组件。
Owl更接近于Vue:它也神奇地跟踪状态属性,但它只在它改变时增加一个内部计数器。请注意,它是通过代理完成的,这意味着它对开发人员是完全透明的。支持添加新key。一旦状态的任何部分被改变,渲染将在下一个微任务(promise队列)中被调度。
7 钩子Hooks
Hooks最近接管了React世界。它们解决了许多看似无关的问题:以可组合的方式将可重用行为附加到组件上,从组件中提取有状态逻辑,或者在组件之间重用有状态逻辑,而无需更改组件层次结构。
下面是一个React useState钩子的例子:
import React, { useState } from "react";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
由于React设计hooks API的方式,它们只适用于函数式的组件。但在这种情况下,它们真的很强大。每个主要的React库都在用钩子重新设计它们的API(例如Redux)。
Vue 2没有钩子,但Vue项目正在开发它的下一个版本,它将以新的组合API为特色。这项工作是基于React钩子引入的新思想。
从React和Vue引入钩子的方式来看,钩子似乎与类组件不兼容。然而,正如Owl钩子所显示的那样,情况并非如此。他们受到React和Vue的启发。例如,useState钩子是以React命名的,但它的API更接近于响应式Vue钩子。
下面是Owl的例子:
import { Component, Owl } from "owl";
import { xml } from "owl/tags";
class Example extends Component {
static template = xml`
<div>
<p>You clicked {count.value} times</p>
<button t-on-click="increment">Click me</button>
</div>`;
count = useState({ value: 0 });
increment() {
this.state.value++;
}
}
由于Owl框架在其早期就有钩子,所以它的主要api从一开始就被设计为与钩子交互。例如,上下文抽象。
三 关于Owl编译模板的说明
本页将解释Owl编译的模板是什么样子的。这是一份技术文档,面向有兴趣了解Owl内部工作原理的开发人员。
一般来说,Owl将模板编译成一个javascript函数(闭包),该函数返回一个函数(“render”函数)。闭包的目的是有一个地方存储模板特定的所有值(特别是“块”)。一旦模板被编译,它的闭包函数被调用一次以获得渲染函数,从那时起,只使用渲染函数。
渲染函数接受一些上下文(和一些附加信息),并以块树的形式返回渲染模板的虚拟dom表示。块树是一种非常轻量级的表示,它只包含模板的动态部分及其结构。它实际上独立于模板的静态部分(包含在闭包捕获的块中)。这意味着在渲染时执行的工作只是收集动态数据,并描述结果的块结构。
它看起来像这样,在伪代码中:
function closure(bdom, helpers) {
// here is some place to put stuff specific to the template, such as
// blocks
...
return function render(context, node, key) {
// only build here all dynamic parts of the template
// build a block tree
return tree;
}
}
现在,让我们看一个例子。考虑以下模板:
<div class="some-class">
<div class="blabla">
<span><t t-esc="state.value"/></span>
</div>
<t t-if="state.info">
<p class="info" t-att-class="someAttribute">
<t t-esc="state.info"/>
</p>
</t>
<SomeComponent value="value"/>
</div>
如果你仔细观察,有5个动态的东西:
- a text value (the first
t-esc
), - a sub block (the
t-if
), - a dynamic attribute (the
t-att-class
attribute), - another text value (the second
t-esc
), - and finally, a sub component
下面是这个模板的编译代码:
function closure(bdom, helpers) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
);
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
return function render(ctx, node, key = "") {
let b2, b3;
let txt1 = ctx["state"].value;
if (ctx["state"].info) {
let attr1 = ctx["someAttribute"];
let txt2 = ctx["state"].info;
b2 = block2([attr1, txt2]);
}
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
return block1([txt1], [b2, b3]);
};
}
闭包中捕获的值捕获模板的静态部分:我们在这里定义了两个块(其中包含一个模板节点,可以在挂载块时进行深度克隆)。然后,渲染函数仅根据上下文描述结果的块树结构。这意味着我们将在渲染时完成的工作量最小化。
然后,当我们想要patch dom时,Owl将使用来自blockdom的patch函数,该函数将对块树进行diff,并在插入新块时深度克隆新块,跟踪每个块的动态部分,并相应地更新它们。
在这种设计中,渲染模板的成本与动态值的数量成正比,而与模板的大小无关。
四 为什么是owl?
普遍的智慧是,一个人不应该重新发明轮子,因为那会浪费精力和资源。在许多情况下,这当然是正确的。编写javascript框架是一个相当大的投资,所以问这个问题是很合乎逻辑的:为什么Odoo决定做OWL,而不是使用标准的/众所周知的框架,比如React或Vue?
正如你所料,这个问题的答案并不简单。但本页讨论的大多数原因都源于一个事实:Odoo是非常模块化的。
这意味着,例如,Odoo的核心部分在运行之前不知道将加载/执行哪些文件,或者UI的状态是什么。因此,Odoo不能依赖于标准的构建工具链。此外,这意味着Odoo的核心部分需要非常通用。换句话说,Odoo并不是一个真正具有用户界面的应用程序。它是一个生成动态用户界面的应用程序。而且大多数框架都不能胜任这项任务。
把赌注押在Owl上并不是一个容易的选择,因为肯定有很多相互冲突的需求,我们想要小心地平衡。选择除知名框架之外的任何框架都必然会引起争议。本页将解释为什么我们仍然相信构建Owl是一项值得努力的工作。
1.对策
的确,我们希望控制我们的技术,因为我们不想依赖Facebook或谷歌,或任何其他大(或小)公司。如果他们决定改变他们的许可证,或者去一个不适合我们的方向,这可能是一个问题。因为Odoo不是一个传统的javascript应用程序,我们的需求可能与大多数其他应用程序大不相同,所以更是如此。
2.类组件
很明显,最大的框架正在远离类组件。有一种隐含的假设认为类组件很糟糕,而函数式编程才是正确的选择。React甚至说类会让开发人员感到困惑。
虽然这有一定的道理,而且组合确实是代码重用的良好机制,但我们相信类和继承是重要的工具。
通过继承在泛型组件之间共享代码是Odoo构建其web客户端的方式。很明显,继承并不是一切罪恶的根源。这通常是一个非常简单和适当的解决方案。最重要的是架构决策。
此外,Odoo在类组件之外还有另一个特定的用途:类的每个方法都为插件提供了一个扩展点。这可能不是一个干净的体系结构模式,但它是一个实用的决定,很好地服务于Odoo:类有时被猴子修补,以从外部添加行为。有点像混合.
使用React或Vue将使对组件进行猴子补丁变得更加困难,因为许多状态隐藏在它们的内部。
3 工具
React或Vue有一个庞大的社区,并且在他们的工具上付出了很多努力。这很棒,但同时,对于Odoo来说,一个相当大的问题是:由于资产是完全动态的(并且可以在用户安装或删除插件时更改),我们需要在生产服务器上拥有所有这些工具。这当然不理想
此外,这使得设置Vue或React工具变得非常复杂:Odoo代码不是一个导入其他文件的简单文件。它一直在变化,资产在不同的环境中捆绑的方式不同。这就是Odoo拥有自己的模块系统的原因,这些模块系统在运行时由浏览器解析。Odoo的动态特性意味着我们经常需要尽可能地延迟工作(换句话说,我们想要一个JIT用户界面!)
我们理想的框架具有最少的(强制性的)工具,这使得它更容易部署。使用没有JSX的React或没有Vue文件的Vue并不是很吸引人。
同时,Owl被设计来解决这个问题:它通过浏览器编译模板,它不需要太多的代码,因为我们使用了每个浏览器内置的XML解析器。Owl可以使用或不使用任何额外的工具。它可以使用模板字符串来编写单个文件组件,并且很容易集成到任何html页面中,只需使用一个简单的
4 基于模板
Odoo将模板作为XML文档存储在数据库中。这是非常强大的,因为它允许使用xpath来定制其他模板。这是odoo的一个非常重要的特性,也是odoo模块化的关键之一。
因此,我们仍然希望在XML文档中编写模板。奇怪的是,没有一个主流框架使用XML来存储模板,尽管它非常方便。
所以,使用React或Vue意味着我们需要做一个模板编译器。对于React,这将是一个编译器,它将接受QWeb模板,并将其转换为React呈现函数。对于Vue,它会将其转换为Vue模板。然后我们还需要捆绑vue模板编译器。
这不仅会很复杂(将一种模板语言编译成另一种语言不是一件容易的事),而且还会对开发人员的体验产生负面影响。在QWeb模板中编写Vue或React组件肯定会很尴尬,而且非常令人困惑。
5 开发者体验
这就引出了以下几点:开发者体验。我们将这一选择视为对未来的投资,我们希望让新入职的开发者尽可能简单。
虽然许多javascript专业人士显然认为react/vue并不难(在某种程度上是真的),但许多非js专业人士也确实被前端世界所淹没:功能组件、钩子和许多其他花哨的词汇。此外,在编译上下文中可用的内容可能很困难,几乎每个框架中都有很多黑魔法。我们以某种方式在底层将各种名称空间连接为一个,并添加各种内部键。快速转换代码。React要求状态转换是深度的,而不是浅层的。
Owl正在非常努力地尝试拥有一个简单而熟悉的API。它使用类。它的反应系统是显性的,而不是隐性的。作用域规则是显而易见的。如果有疑问,我们宁可不实现某个功能。
它当然不同于React或Vue,但同时,对于有经验的开发人员来说,它也很容易熟悉。
6 即时编译
在前端领域也有一个明显的趋势,即尽可能提前编译代码。大多数框架都会提前编译模板。现在Svelte正在尝试编译JS代码,这样它就可以把自己从bundle中移除。
注:Svelte是一种构建web应用程序的新方法。它是一个编译器,它接受声明性组件,并将它们转换为高效的JavaScript,从而快速更新DOM。 svelte中文意思是苗条的,读音类似: 绍特,瘦的, 编译出来的文件比较小,因为不带运行时,现在比较流行,github上有70k start了,vue 是200k
这对于许多用例来说当然是合理的。然而,这并不是Odoo所需要的:Odoo将从数据库中获取模板,并且只需要在最后一刻编译它们,因此我们可以应用所有必要的xpath
更重要的是:Odoo需要能够在运行时生成(和编译)模板。目前,Odoo表单视图解释xml描述。但是表单视图代码需要做很多复杂的操作。使用Owl,我们将能够将视图描述转换为QWeb模板,然后编译并立即使用它。
7 响应式
我们觉得在其他框架中还有一些设计选择不是最优的。例如,响应式系统。我们喜欢Vue这样做的方式,但它有一个缺陷:它不是真正可选的。实际上有一种方法可以通过冻结状态来退出反应系统,但是,它被冻结了。
当然在某些情况下,我们需要一个状态,它不是只读的,不被观察到。例如,想象一个电子表格组件。它可能有一个非常大的内部状态,并且它确切地知道何时需要重绘(基本上,每当用户执行某些操作时)。然后,观察它的状态非常消耗CPU和内存的性能。
8 并发性
许多应用程序都很乐意在执行新的异步操作时简单地显示一个微调器,但是Odoo想要一种不同的用户体验:大多数异步状态更改直到准备好才显示。这有时被称为并发模式:UI在内存中呈现,只有当它准备好时才显示(并且只有当它没有被后续的用户操作取消时)。
React现在有一个实验性的并发模式,但是在Owl启动的时候还没有准备好。Vue并没有真正等效的API(我们不需要悬念)。
而且,React并发模式使用起来很复杂。并发是以前的Odoo js框架(小部件)少有的优点之一,我们觉得Owl现在有一个非常强大的并发模式,它既简单又强大。
9 结论
这个冗长的讨论表明,当前的标准框架没有适合我们的需要,有许多小的和不那么小的原因。这完全没问题,因为他们各自选择了一套不同的权衡。
然而,我们觉得在框架世界中仍然有一些不同的空间。对于一个与Odoo兼容的框架。
这就是我们建造Owl🦉的原因。