第一章 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,{{partner.image_256}}" 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>

效果如下图:

results matching ""

    No results matching ""