第一章 Hello, OWL!
OWL是什么
OWL(Odoo Web Libary)是Odoo从14.0开始引入的前端框架, OWL区别于之前版本的QWeb技术, 与近些年来前端流行的框架(React, Vue, Angular, Backbone)更为相似. 如果你对现在的这些流行的前端框架有所了解,那么你一定明白这些框架的目的都在于简化之前那些由javascript来处理的琐碎的工作. 这些框架最大程度地解耦了你的HTML和Javascript代码, 动不动写几百行代码用来操作HTML DOM元素与监听事件的时代成为了过去式.
举个非常简单的例子,我们这里都有一段HTML代码:
<button id="countButton">Increment Count</button>
<button id="clearButton">Clear Count</button>
<div id="results">0</div>
这段代码定义了2个按钮,一个用来增加计数, 一个用来归零. 如果用我们过去的代码方式, 这段代码应该这么写:
// ew, gross
const clicks = 0;
const countButton = document.querySelector("#countButton");
myButton.addEventListener("click", function() {
clicks += 1;
const results = document.querySelector("#results");
results.innerHTML = clicks;
});
const clearButton = document.querySelector("#clearButton");
clearButton.addEventListener("click", function() {
clicks = 0;
const results = document.querySelector("#results");
results.innerHTML = click;
});
这只是一小段代码, 随着项目量的增长, 这些代码的可读性将变得很差. 现在, 让我们来看一下现代框架们是如何处理的(OWL为例):
<button id="countButton" t-on-click="state.count++">Increment Count</button>
<button id="clearButton" t-on-click="state.count = 0">Clear Count</button>
<div id="results" t-esc="state.count"/>
const { Component, useState } = owl;
class ClickComponent extends Component {
state = useState({ count: 0 });
}
DOM操作和事件监听完全由框架来帮我们处理了, 代码变得非常简单.
Odoo为啥创建了OWL而没有使用React, Vue等既有框架?
哈, 这个可以看官方的回答
在笔者看来无非就两个原因, 一是现有框架不能完全满足Odoo的需求, 二是作为一个帮技术控,有足够的自信自己做一套出来, 同时也不想被其他框架制约.
主要特点
与旧框架相比,OWL香在以下几点, 写起来更简单, 更优雅, 可读性也更高.
生命周期的组件
在过去, 我们需要监控DOM的状态,以防止我们的代码运行时符合预期. 而现在, 我们知道我们的组件会在页面启动时加载, 在页面跳转时消亡, 而且我们有很多钩子用来处理这些事情, 而不再有$(document).ready了.
响应式虚拟DOM
如果你看了前面的代码, 那么你就看到了响应式绑定的好处, 我们只需要考虑如何组织处理数据, 而不用再关心如何操作处理DOM元素, 所有的DOM操作代码都被移除了,当我们的数据发生变化时, 页面自动进行了更新. 很多现代框架都提供了虚拟DOM用来跟踪前端结构发生的变化, 尤其是事件绑定的场景.
更好的可读性
移除了操作DOM的代码之后, 代码的可读性自然就提高了, 而且我们更容易把精力放到项目本身的逻辑处理中,对于编写测试脚本和测试用例来说显然也变得更容易.
开始学习
虽然OWL在15.0发布时又发生了变化, 但是我们的学习还是从14.0时开始, 下面的例子即基于odoo14.0, 后面我们会介绍15.0究竟与14.0有何不同.
我们依旧使用我们的书店模块, 这次主要工作在static/src/js目录中, 我们创建一个新文件夹components用来存放我们的组件代码.
OWL通过定义组件(Component)来渲染模板,加载数据, 加载子组件等工作. 在HTML中, 我们有header, div, span, textarea等标签来供我们使用, 当我们要创建一个OWL组件时,我们需要思考,当我们创建了这个组件,对我们的项目有什么好处.
本例子中,我们将创建一个组件用来显示销售单下的某个客户的订单历史信息.
创建和注册js类
我们在components文件夹下创建一个PartnerOrderSummary.js文件.
odoo.define("book_store.PartnerOrderSummary", function (require) {
'use strict';
const { Component } = owl;
class PartnerOrderSummary extends Component {
};
Object.assign(PartnerOrderSummary,{
template: "book_store.PartnerOrderSummary"
});
});
跟14.0之前的版本一样, 所有的js文件都要在templates文件中注册到assets中:
<template id="assets_backend_book_store" inherit_id="web.assets_backend" name="book_store">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/book_store/static/src/js/component/PartnerOrderSummary.js"></script>
</xpath>
</template>
为component创建模板
现在我们来为我们的组件创建一个模板文件(同样位于components文件夹内):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="book_store.PartnerOrderSummary" owl="1">
<div>My cool new widget</div>
</t>
</templates>
同样的,我们需要把模板文件放到QWeb中
"qweb":[
"static/src/js/components/PartnerOrderSummary.xml"
],
在销售单中显示组件
现在我们需要把我们的组件显示到销售单中, 首先,我们需要更新我们的依赖:
'depends': ['sale_management'],
重载表单的渲染方法挂载我们的组件
想要在销售单中显示我们的组件,最简单的办法就是重载表单的渲染方法, 下面我们将修改PartnerOrderSummary.js的文件内容:
odoo.define("book_store.PartnerOrderSummary", function (require) {
'use strict';
const FormRenderer = require("web.FormRenderer");
const { Component } = owl;
class PartnerOrderSummary extends Component {
};
Object.assign(PartnerOrderSummary, {
template: "book_store.PartnerOrderSummary"
});
FormRenderer.include({
async _render() {
await this._super(...arguments);
for (const element of this.el.querySelectorAll(".o_partner_order_summary")) {
(new ComponentWrapper(this, PartnerOrderSummary))
.mount(element)
}
}
});
});
如果你没有涉足过odoo的前端开发,那么这段代码来说阅读起来可能有点困难, 不过没关系, 我们只要知道通过ComponentWrapper, 我们可以在把要一个组件挂载到任何元素上.
(new ComponentWrapper(this, PartnerOrderSummary))
.mount(element)
添加div元素到销售单表单视图
从上面的代码中我们可以看到,要显示的组件需要定位到含有o_partner_order_summary样式的div标签上, 因此,接下来我们添加这个标签:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="payment_term_id" position="after">
<div class="o_partner_order_summary" colspan="2"/>
</field>
</field>
</record>
</odoo>
然后把我们的视图文件放到_mainfest_.py文件中:
'data': [
'security/ir.model.access.csv',
'views/views.xml',
"views/sale.xml"
]
效果如下:
优化我们的组件
让我们的组件看起来更美观一些. 现在使用了一些假数据:
<templates xml:space="preserve">
<t t-name="book_store.PartnerOrderSummary" owl="1">
<div class="center" style="width: 100%; text-align: center; border: 1px solid #cecece; padding: 2rem 20%; margin: 12px 0;">
<img src="#" width="75px" height="75px" style="background-color: #ccc; border-radius: 50%; margin-bottom: 10px;"/>
<!-- Customer name -->
<p style="font-size: 16px; color: #4d4b4b;">
<strong>Kevin Kong</strong>
</p>
<!-- Address -->
<p style="font-size: 12px; color: #8c8787;">
<i class="fa fa-map-marker" style="padding-right: 4px;"/>
<span>Qingdao</span>
</p>
<!-- Grid of previous order stats -->
<div class="row" style="padding-top: 20px;">
<div class="col-6" style="border-right: 1px solid #ccc;">
<p style="font-size: 20px;">
<strong>35</strong>
</p>
<p style="font-size: 12px; color: #8c8787;">Orders</p>
</div>
<div class="col-6">
<p style="font-size: 20px;">
<strong>$97,183.50</strong>
</p>
<p style="font-size: 12px; color: #8c8787;">Total Sales</p>
</div>
</div>
</div>
</t>
</templates>
最后一步, 关联我们的数据
最后一步就是关联我们的客户数据, OWL通过使用state对象来跟踪组件, 所以我们在组件的构造函数中创建一个partner对象来设置数据:
const { useState } = owl.hooks;
class PartnerOrderSummary extends Component {
partner = useState({});
constructor(self, partner) {
super();
this.partner = partner;
}
}
组件在页面渲染的时候被初始化, 这个时候我们查询partner的数据:
FormRenderer.include({
async _renderView() {
await this._super(...arguments);
for(const element of this.el.querySelectorAll(".o_partner_order_summary")) {
this._rpc({
model: "res.partner",
method: "read",
args: [[this.state.data.partner_id.res_id]]
}).then(data => {
(new ComponentWrapper(
this,
PartnerOrderSummary,
useState(data[0])
)).mount(element);
});
}
}
});
最后我们更新组件的模板文件 , 引入真正的数据:
<templates xml:space="preserve">
<t t-name="book_store.PartnerOrderSummary" owl="1">
<div class="center" style="width: 100%; text-align: center; border: 1px solid #cecece; padding: 2rem 20%; margin: 12px 0;">
<img t-attf-src="data:image/jpg;base64," width="75px" height="75px" style="background-color: #ccc; border-radius: 50%; margin-bottom: 10px;"/>
<!-- Customer name -->
<p style="font-size: 16px; color: #4d4b4b;">
<strong t-esc="partner.name"/>
</p>
<!-- Address -->
<p style="font-size: 12px; color: #8c8787;">
<i class="fa fa-map-marker" style="padding-right: 4px;"/>
<span t-esc="partner.city"/>
<span t-esc="partner.zip" style="margin-left: 5px;"/>
</p>
<!-- Grid of previous order stats -->
<div class="row" style="padding-top: 20px;">
<div class="col-6" style="border-right: 1px solid #ccc;">
<p style="font-size: 20px;">
<strong t-esc="partner.sale_order_count"/>
</p>
<p style="font-size: 12px; color: #8c8787;">Orders</p>
</div>
<div class="col-6">
<p style="font-size: 20px;">
<strong t-esc="partner.sale_order_revenue" t-options='{"widget": "monetary"}'/>
</p>
<p style="font-size: 12px; color: #8c8787;">Total Sales</p>
</div>
</div>
</div>
</t>
</templates>
效果如下图: