什么是OWL
OWL是 Odoo 在版本14中一个大动作,是Odoo Web Library的缩写,简单的说是一套前端开发框架。官方总结主要特性有:
- 声明式的组件系统
- 基于钩子的响应式系统
- 默认为并发模式
- 存储和前端路由
听起来有些类似流行的 Web 框架 Angular、Vue、React等,那么 Odoo 为什么还要重造轮子,请收看《Odoo 有话说》。
上图为10月6日Odoo 15大会中给出的OWL修订后的路线图,可以看到在 v15中并没有完全弃用老的前端代码,但在 Odoo 16中将会彻底切换为 OWL。
以下窃取一个示例来说明 OWL 的优势:
在界面中添加两个按钮,在点击操作时一个进行累加,另一个做清除将值重置为0,实时的结果显示在一个 div 中,普通HTML 的实现方式为:
1 2 3 4 |
<button id="countButton">累加</button> <button id="clearButton">清零</button> <div id="results">0</div> |
接下来就写编写 Javascript 来实现相应的逻辑了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let clicks = 0; const countButton = document.querySelector("#countButton"); countButton.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 = clicks; }); |
对于前端朋友这些简直是家常便饭了,这只是一个很小的例子,如果逻辑更为复杂,代码量可想而知。下面是使用现代框架OWL进行实现的代码:
1 2 3 4 |
<button id="countButton" t-on-click="state.count++">累加</button> <button id="clearButton" t-on-click="state.count = 0">清零</button> <div id="results" t-esc="state.count"/> |
好像并没有变简单,重点在JS 代码:
1 2 3 4 5 |
const { Component, useState } = owl; class ClickComponent extends Component { state = useState({ count: 0 }); } |
怎么样?是不是很丝滑?框架为我们处理了事件监听、DOM 操作这个麻烦事,让代码看起来也非常之简洁。
闲言少絮,我们直接进入实战,看看来 Odoo 中如何进行实操。
准备工作
Odoo 14开始内置有OWL,因此不需要再进行额外的安装。首先我们需要新建一个模块来进行 OWL 开发的演示,基本目录结构如下:
1 2 3 4 5 6 7 8 9 |
owl_demo ├── __init__.py ├── __manifest__.py └── static ├── description │ └── icon.png └── src └── js └── components |
声明文件示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "name": "OWL开发演示", "summary": "示例OWL开发模块。", "description": "用于演示OWL模块的开发。", "author": "Alan Hou", "website": "https://alanhou.org", "category": "Tutorials", "version": "15.0.0.1", "depends": ["base"], "demo": [], "data": [], } |
添加组件
OWL的通过定义Component
类的方式来进行编码,可以看成是 web 组件。组件中有模板、数据绑定及子组件。
我们都熟知在 HTML中有 header
, div
, span
, textarea
等标签。在 OWL 中,我们可以自己创造这些标签,比如在项目管理系统中可以使用project
和 task
标签来展示信息,或是在系统中使用contact
标签来显示用户或客户。本例中我们创建一个组件在销售订单的客户信息下展示其订单历史信息。新建组件需要经过2步:
1. 创建并注册js类
新组件位于static/src/js/components/
下,命名为PartnerOrderSummary.js
,内容如下:
1 2 3 4 5 6 7 8 9 10 11 |
odoo.define("owl_demo.PartnerOrderSummary", function (require) { const {Component} = owl; class PartnerOrderSummary extends Component { // }; Object.assign(PartnerOrderSummary, { template: "owl_demo.PartnerOrderSummary" }); }); |
Odoo 14
在Odoo中所有的 Javascript 代码通过继承assets模板并添加script
标签来进行注册。本例我们在模块的根目录下创建assets.xml
文件:
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="utf-8"?> <odoo> <template id="assets_backend_inherit" inherit_id="web.assets_backend"> <xpath expr="script[last()]" position="after"> <script src="/owl_demo/static/src/js/components/PartnerOrderSummary.js"/> </xpath> </template> </odoo> |
然后在__manifest__.py
文件中添加assets.xml
:
1 2 3 4 5 6 |
{ ... "data": [ "assets.xml" ], } |
Odoo 15
Odoo 15中以上代码就行不通了,会出现ValueError: External ID not found in the system: web.assets_backend
这样的报错,这是在为 Odoo 15开发模块时所遇到的第一个坑。
1 2 3 4 5 6 7 8 9 |
{ ... 'assets': { 'web.assets_backend': [ 'owl_demo/static/src/js/components/PartnerOrderSummary.js', ], ... } } |
注意:Odoo 15对于前端中JS文件及模板文件等的注册进行了较大的调整,将这些资源类的注册统一纳入到了声明文件的 assets 下,有web.assets_backend、web.assets_frontend、web.assets_common、web.assets_qweb等,下面注册小部件时就会使用到web.assets_qweb。
2. 为组件创建template xml文件
紧接着我们要创建一个XML模板。读者可能已经注意到在前面的代码中我们添加过一个template
属性:
1 2 3 |
Object.assign(PartnerOrderSummary, { template: "owl_demo.PartnerOrderSummary" }); |
这里template
就是XML模板的名称。为方便管理,我们将模板文件放到 JS 的相同目录下,即static/src/js/components/PartnerOrderSummary.xml
:
1 2 3 4 5 6 |
<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="owl_demo.PartnerOrderSummary" owl="1"> <div>耀眼的小部件</div> </t> </templates> |
有关xml:space=”preserve”更多的解释可参见Understanding xml:space中的说明,简单地说就是保留节点中的空格,多行时每一行需要用标签包裹。
类似JS 类,我们也需要注册这一模板文件。注册方法是在__manifest__.py
文件中添加一个qweb
配置:
Odoo 15
1 2 3 4 5 6 7 8 |
{ 'assets': { ... 'web.assets_qweb': [ 'owl_demo/static/src/js/components/PartnerOrderSummary.xml', ], } } |
Odoo 14
1 2 3 4 5 6 |
{ ... "qweb": [ "static/src/js/components/PartnerOrderSummary.xml" ] }<code class="python hljs python-code"> |
在销售订单中显示我们的组件
现在我们已经开了一个显示一些文本的小模块,那么如何在销售订单的表单视图中显示它呢?
1. 修改模块的依赖
首先要在模块声明文件中添加对sales模块的依赖:
1 2 3 4 5 |
{ ... "depends": ["sale", "sale_management"], ... } |
2. 重载表单渲染器挂载组件
更新模块来确保Sales已安装,接下来就将组件添加到销售订单的表单中了。
有很多种实现的方式,这里采用最简单的页面加载时挂载的方式。
我们需要修改 js 文件来扩展Odoo内核的表单渲染器,让其查找特定的html class并自动挂载到该元素中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
odoo.define("owl_demo.PartnerOrderSummary", function (require) { const FormRenderer = require("web.FormRenderer"); const {Component} = owl; const {ComponentWrapper} = require("web.OwlCompatibility"); /** * 这一OWL组件负责添加用于显示指定客户历史订单详情的订单汇总小部件 */ class PartnerOrderSummary extends Component { // }; /** * 对小部件注册属性 */ Object.assign(PartnerOrderSummary, { template: "owl_demo.PartnerOrderSummary" }); /** * 重载表单渲染器,对任何包含o_partner_order_summary这一class 的div挂载当前组件 */ FormRenderer.include({ _render: async function() { await this._super(...arguments); for (const element of this.el.querySelectorAll(".o_partner_order_summary")) { (new ComponentWrapper(this, PartnerOrderSummary)) .mount(element) } } }); }); |
这里包含了比较多的知识,简单地说就是我们对带有o_partner_order_summary
的元素挂载所开发的OWL组件。只需要通过下面这样就可以将组件挂载到指定的对象元素上:
1 2 |
(new ComponentWrapper(this, PartnerOrderSummary)) .mount(element) |
3. 在销售订单的表单中添加一个div
我们在模块的根目录下创建一个views.xml
文件用于重载销售订单的视图,添加我们的div。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?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> |
相应地我们需要修改__manifest__.py
文件:
1 2 3 4 5 6 7 8 |
{ ... "data": [ ... "views.xml" ], ... } |
美化小部件
此时更新模块就可以看到新增的模块了,下面我们做进一步的美化。
1. 设计小部件
先使用 mock 数据来调整样式。这里使用的不是真实数据,但可以看到最终的效果。推荐将 css 放到独立的文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="owl_demo.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>Gemini Furniture</strong></p> <!-- Address --> <p style="font-size: 12px; color: #8c8787;"> <i class="fa fa-map-marker" style="padding-right: 4px;"/> <span>Fairfield</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> |
2. 数据对接
最后就是要对接真实的数据。OWL通过state对象来跟踪所有的数据,因此我们需要在构造时添加partner属性并设置数据。
1 2 3 4 5 6 7 8 9 10 |
const { useState } = owl.hooks; class PartnerOrderSummary extends Component { partner = useState({}); constructor(self, partner) { super(); this.partner = partner; } } |
在表单视图渲染时会初始化该组件。这样我们可以获取获取partner信息并将其传递给构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
FormRenderer.include({ _renderView: async function () { 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); }); } } }); |
以上代码中首先我们在页面中查找包含o_partner_order_summary的元素。然后对后端进行rpc调用获取partner数据并将其关联至当前订单(使用this.state.data
来表示当前记录)。返回数据后挂载至组件中。
现在,我们可以在XML视图中通过变量partner来使用partner记录中的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="owl_demo.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.total_invoiced" t-options='{"widget": "monetary"}'/> </p> <p style="font-size: 12px; color: #8c8787;">Total Sales</p> </div> </div> </div> </t> </templates> |
这个视图会将运行t-
语句、拉取真实数据填充到视图中。最简单的是使用t-esc
将数据直接打印到 div 标签中。可以在属性名前添加t-attf-
来修改具体的属性,然后在该字符串中访问变量。
例如,可以像下面这样显示图片:
1 2 3 4 5 |
<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;"/> |
或像下面这样显示城市和邮编:
1 2 3 4 5 |
<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> |
此时更新模块就可以看到我们的自定义小部件了:
参考资料:
后记
本文是Alan 在做的一个尝试,之前已表明过正常情况下不会再翻译《开发指南》和《手册》,其中有版权的考虑,也有对社区发展阶段的判断。所以准备出一些集锦文章,内容参考Odoo Mates, Cybrosys、Greg Moss和Yenthe Van Ginneken等视频、文章,同时结合官方文档。未来有机会再整理一些更贴合实战的文章或视频。
关于Odoo 15,粗略看来对前端进行了比较大的调整,所以可能很多插件都需要做比较大的修改才能兼容。文章更多是做一种探讨和研究,线上使用大家还是再等半年以上吧!