构建接口扩展(Building Interface Extensions)javascript
本指南是关于为Odoo的web客户建立模块。css
要建立有Odoo的网站,请参见创建网站;要添加业务功能或扩展Odoo的现有业务系统,请参见构建模块。html
警告:
该指南须要如下知识:
Javascript 、jQuery、Underscore.js
同时也须要安装 Odoo 和 Git。
让咱们从一个简单的Odoo模块开始,它包含基本的web组件配置,并让咱们测试web框架。
示例模块能够在线下载,可使用如下命令下载:java
$ git clone http://github.com/odoo/petstore
这将在您执行命令的地方建立一个petstore文件夹。而后须要将该文件夹添加到Odoo的addons路径中,建立一个新的数据库并安装oepetstore模块。git
若是您浏览petstore文件夹,您应该看到如下内容:github
oepetstore |-- images | |-- alligator.jpg | |-- ball.jpg | |-- crazy_circle.jpg | |-- fish.jpg | `-- mice.jpg |-- __init__.py |-- oepetstore.message_of_the_day.csv |-- __manifest__.py |-- petstore_data.xml |-- petstore.py |-- petstore.xml `-- static `-- src |-- css | `-- petstore.css |-- js | `-- petstore.js `-- xml `-- petstore.xml
模块已经包含了各类服务器定制。稍后咱们将回到这些内容,如今让咱们关注与web相关的内容,在静态文件夹(static)中。web
在Odoo模块的“web”端中使用的文件必须放置在静态文件夹中,这样它们就能够在web浏览器中使用,而浏览器以外的文件也不能被浏览器获取。src/css、src/js和src/xml子文件夹是常规的,并非绝对必要的。数据库
oepetstore/static/css/petstore.css
目前为空,将为宠物店(pet store)内容保留CSS。api
oepetstore/static/xml/petstore.xml
大部分也是空的,将保存QWeb模板。数组
oepetstore/static/js/petstore.js
最重要(也是最有趣的)部分,包含javascript应用程序的逻辑(或者至少是它的web浏览器端)。它如今应该是:
openerp.oepetstore = function(instance, local) { //特别注意:红色部分在开发文档中10.0版本中用odoo关键字,可是测试时没法经过,必须是openerp,估计是还没有彻底支持odoo关键字 var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; local.HomePage = instance.Widget.extend({ start: function() { console.log("pet store home page loaded"); }, }); instance.web.client_actions.add( 'petstore.homepage', 'instance.oepetstore.HomePage'); }
它只在浏览器的控制台打印一个小消息。
静态文件夹中的文件,须要在模块中定义,以便正确加载它们。src/xml中的全部内容都在__manifest . __中定义。在petstore.xml或相似的文件中定义或引用src/css和src/js的内容。
警告
全部的JavaScript文件都被链接和缩小以提升应用程序的加载时间。
其中一个缺点是,随着单个文件的消失,调试变得更加困难,并且代码的可读性也大大下降。能够经过启用“开发者模式”来禁用此过程:
登陆到您的Odoo实例(默认用户admin密码admin)打开用户菜单(在Odoo屏幕的右上角)并选择Odoo,而后激活开发者模式:
Javascript没有内置模块。所以,在不一样文件中定义的变量都会混合在一块儿,并可能发生冲突。这引起了各类模块模式,用于构建干净的名称空间并限制命名冲突的风险。 Odoo框架使用一种这样的模式来定义Web插件中的模块,以便命名空间代码和正确地命令其加载。
oepetstore/static/js/petstore.js
文件中包含一个模块声明,代码以下:
openerp.oepetstore = function(instance, local) { local.xxx = ...; }
在Odoo网站中,模块被声明为在全局odoo(请改为openerp)变量上设置的函数。该函数的名称必须与模块名称(在这里为oeststore)相同,以便框架能够找到它,并自动初始化它。
当Web客户端加载你的模块时,它会调用根函数并提供两个参数:
第一个参数(instance)是Odoo Web客户端的当前实例,它容许访问由Odoo(网络服务)定义的各类功能以及由内核或其余模块定义的对象。
第二个参数(local)是您本身的本地名称空间,由Web客户端自动建立。应该能够从模块外部访问的对象和变量(不管是由于Odoo Web客户端须要调用它们,仍是由于其余人可能想要定制它们)应该在该名称空间内设置。
就像模块同样,而且与大多数面向对象的语言相反,JavaScript不会构建在classes中,尽管它提供了大体相同(若是是较低级别和更详细的)机制。
为了简单和开发人员友好,Odoo web提供了一个基于John Resig的简单JavaScript继承的类系统。
经过调用odoo.web.Class()的extend()方法来定义新的类:
var MyClass = instance.web.Class.extend({ say_hello: function() { console.log("hello"); }, });
extend()方法须要一个描述新类的内容(方法和静态属性)的字典。在这种状况下,它只会有一个不带参数的say_hello方法。
类使用new运算符实例化:
var my_object = new MyClass(); my_object.say_hello(); // print "hello" in the console
实例的属性能够经过如下方式 this 访问:
var MyClass = instance.web.Class.extend({ say_hello: function() { console.log("hello", this.name); }, }); var my_object = new MyClass(); my_object.name = "Bob"; my_object.say_hello(); // print "hello Bob" in the console
经过定义init()方法,类能够提供初始化程序来执行实例的初始设置。初始化程序接收使用新运算符时传递的参数:
var MyClass = instance.web.Class.extend({ init: function(name) { this.name = name; }, say_hello: function() { console.log("hello", this.name); }, }); var my_object = new MyClass("Bob"); my_object.say_hello(); // print "hello Bob" in the console
也能够经过在父类上调用extend()来建立现有(使用定义的)类的子类,如同子类Class()所作的那样:
var MySpanishClass = MyClass.extend({ say_hello: function() { console.log("hola", this.name); }, }); var my_object = new MySpanishClass("Bob"); my_object.say_hello(); // print "hola Bob" in the console
当使用继承覆盖方法时,可使用this._super()调用原始方法:
var MySpanishClass = MyClass.extend({ say_hello: function() { //已覆盖的方法 this._super(); //调用父类中的原始方法,即“hello 。。。” console.log("translation in Spanish: hola", this.name); }, }); var my_object = new MySpanishClass("Bob"); my_object.say_hello(); // print "hello Bob \n translation in Spanish: hola Bob" in the console
_super不是一个标准的方法,它被设置为当前继承链中的一个方法(若是有的话)。它只在方法调用的同步部分中定义,用于异步处理程序(在网络调用或setTimeout回调以后)应该保留对其值的引用,所以不该经过如下方式访问它:
// 如下调用会产生错误 say_hello: function () { setTimeout(function () { this._super(); }.bind(this), 0); } // 如下方式正确 say_hello: function () { // 不能忘记 .bind() var _super = this._super.bind(this); setTimeout(function () { _super(); }.bind(this), 0); }
Odoo web 客户端捆绑了jQuery以实现简单的DOM操做。它比标准的W3C DOM2更有用,而且提供了更好的API,但不足以构成复杂的应用程序,致使难以维护。 很像面向对象的桌面UI工具包(例如Qt,Cocoa或GTK),Odoo Web使特定组件负责页面的各个部分。在Odoo网站中,这些组件的基础是Widget()类,它是专门处理页面部分并显示用户信息的组件。
初始演示模块已经提供了一个基本的widget:
local.HomePage = instance.Widget.extend({ start: function() { console.log("pet store home page loaded"); }, });
它扩展了Widget()并重载了标准方法start(),它与以前的MyClass很像,如今作的不多。
该行在文件末尾:
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
将咱们的widget注册为客户端操做。客户端操做将在稍后解释,如今这只是当咱们选择
菜单时,能够调用和显示咱们的窗口小部件。警告
因为该组件将从咱们的模块外部调用,Web客户端须要其“彻底限定(规范)”名称,而不是任意名称。
local.HomePage = instance.Widget.extend({ start: function() { this.$el.append("<div>Hello dear Odoo user!</div>"); }, });
当您打开
时,此消息将显示。注意
要刷新Odoo Web中加载的JavaScript代码,您须要从新加载页面(升级一下模块)。没有必要从新启动Odoo服务器。
HomePage Widget 由Odoo Web使用并自动管理。要学习如何从头开始使用Widget,咱们来建立一个新Widget:
local.GreetingsWidget = instance.Widget.extend({ start: function() { this.$el.append("<div>We are so happy to see you again in this menu!</div>"); }, });
如今咱们可使用GreetingsWidget的appendTo()方法将咱们的GreetingsWidget添加到主页:
local.HomePage = instance.Widget.extend({ start: function() { this.$el.append("<div>Hello dear Odoo user!</div>"); var greeting = new local.GreetingsWidget(this); return greeting.appendTo(this.$el); }, });
HomePage首先将其本身的内容添加到其DOM根目录;
HomePage而后实例化GreetingsWidget ;
最后,它告诉GreetingsWidget将本身的部分插入到GreetingsWidget中。
当调用appendTo()方法时,它会要求小部件(widget,如下将的小部件就是widget)将自身插入指定位置并显示其内容。在调用appendTo()期间,将调用start()方法。
要查看显示界面下发生了什么,咱们将使用浏览器的DOM Explorer。但首先让咱们稍微修改咱们的小部件,以便经过向它们的根元素添加一个类来更轻松地找到它们的位置:
local.HomePage = instance.Widget.extend({ className: 'oe_petstore_homepage', ... }); local.GreetingsWidget = instance.Widget.extend({ className: 'oe_petstore_greetings', ... });
若是您能够找到DOM的相关部分(右键单击文本而后检查元素),它应该以下所示:
<div class="oe_petstore_homepage"> <div>Hello dear Odoo user!</div> <div class="oe_petstore_greetings"> <div>We are so happy to see you again in this menu!</div> </div> </div>
它清楚地显示了由Widget()自动建立的两个<div>元素,由于咱们在它们上面添加了一些类。
咱们也能够看到咱们本身添加的两个消息控制器。
最后,注意GreetingsWidget实例的<div class =“oe_petstore_greetings”>元素位于表明HomePage实例的<div class =“oe_petstore_homepage”>中,这是由于咱们追加了该元素。
在上一部分中,咱们使用如下语法实例化了一个小部件:
new local.GreetingsWidget(this); //括号内对象是指greetingswidget实例化后归谁全部。
第一个参数是 this,在这种状况下是一个HomePage实例。这告诉小部件被建立,其余小部件是其父项。
正如咱们所看到的,小部件一般由另外一个小部件插入到DOM中,并在其余小部件的根元素内插入。这意味着大多数小部件是另外一个小部件的“部分”,并表明它存在。咱们将容器称为父项,并将包含的小部件称为子项。
因为技术和概念上的多重缘由,小部件有必要知道谁是其父类以及谁是子类。
getParent() 能够用来获取小部件的父级:
local.GreetingsWidget = instance.Widget.extend({ start: function() { console.log(this.getParent().$el ); // will print "div.oe_petstore_homepage" in the console }, });
getChildren() 能够用来获取其子女的名单:
local.HomePage = instance.Widget.extend({ start: function() { var greeting = new local.GreetingsWidget(this); greeting.appendTo(this.$el); console.log(this.getChildren()[0].$el); // will print "div.oe_petstore_greetings" in the console }, });
当重写小部件的init()方法时,将父项传递给this._super()调用是很是重要的,不然关系将没法正确设置:
local.GreetingsWidget = instance.Widget.extend({ init: function(parent, name) { this._super(parent); this.name = name; }, });
最后,若是小部件没有父项(例如,由于它是应用程序的根小部件),则能够将null做为父项提供:
new local.GreetingsWidget(null);
若是您能够向用户显示内容,则应该也能够将其删除。这是经过destroy()方法完成的:
greeting.destroy();
当一个小部件被销毁时,它将首先对其全部子项调用destroy()。而后它从DOM中删除本身。若是你已经在init()或start()中设置了永久结构,必须明确清除它们(由于垃圾回收器不会处理它们),你能够重写destroy()。
危险
当覆盖destroy()时,必须始终调用_super(),不然即便没有显示错误,小部件及其子项也没有正确清理,从而可能会发生内存泄漏和“意想不到的事件”。
在上一节中,咱们经过直接操做(并添加)DOM来将内容添加到咱们的小部件:
this.$el.append("<div>Hello dear Odoo user!</div>");
这容许生成和显示任何类型的内容,但在生成大量DOM时会很难处理(大量重复,引用问题......)。
与许多其余环境同样,Odoo的解决方案是使用模板引擎。 Odoo的模板引擎被称为QWeb。
QWeb是一种基于XML的模板语言,与Genshi,Thymeleaf或Facelets相似。它具备如下特色:
使用QWeb代替现有的JavaScript模板引擎的原理是预先存在的(第三方)模板的可扩展性,就像Odoo视图同样。
大多数JavaScript模板引擎是基于文本的,这排除了容易的结构可扩展性,其中基于XML的模板引擎能够经过使用例如通用数据库XPath或CSS以及树型变动DSL(甚至只是XSLT)。这种灵活性和可扩展性是Odoo的核心特征,丢失它被认为是不可接受的。
首先让咱们在几乎空白的地方定义一个简单的QWeb模板,在如下文件进行操做:
oepetstore/static/src/xml/petstore.xml
<?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> <t t-name="HomePageTemplate"> <div style="background-color: red;">This is some simple HTML</div> </t> </templates>
local.HomePage = instance.Widget.extend({ start: function() { this.$el.append(QWeb.render("HomePageTemplate")); }, });
QWeb.render()查找指定的模板,将其呈现为一个字符串并返回结果。
可是,由于Widget()对QWeb有特殊的集成,因此模板能够经过它的模板属性直接设置在Widget上:
local.HomePage = instance.Widget.extend({ template: "HomePageTemplate", start: function() { ... }, });
尽管结果看起来类似,但这些用法之间有两点区别:
警告
模板应该有一个非t根元素,特别是若是它们被设置为一个小部件的模板。若是有多个“根元素”,结果是未定义的(一般只有第一个根元素将被使用,其余元素将被忽略)。QWeb模板能够被赋予数据而且能够包含基本的显示逻辑。
对于显式调用QWeb.render(),模板数据做为第二个参数传递:
QWeb.render("HomePageTemplate", {name: "Klaus"});
将模板修改成:
<t t-name="HomePageTemplate"> <div>Hello <t t-esc="name"/></div> </t>
最终结果为:
<div>Hello Klaus</div>
当使用Widget()的集成时,不可能为模板提供额外的数据。该模板将被赋予一个单一的窗口小部件上下文变量,引用在start()被调用以前被渲染的窗口小部件(窗口小部件的状态基本上是由init()设置的):
<t t-name="HomePageTemplate"> <div>Hello <t t-esc="widget.name"/></div> </t>
local.HomePage = instance.Widget.extend({ template: "HomePageTemplate", init: function(parent) { this._super(parent); this.name = "Mordecai"; }, start: function() { }, });
结果为:
<div>Hello Mordecai</div>
咱们已经看到了如何渲染QWeb模板,如今让咱们看看模板自己的语法。
QWeb模板由常规XML和QWeb指令组成。 QWeb指令声明了以t-开头的XML属性。
最基本的指令是t-name,用于在模板文件中声明新模板:
<templates> <t t-name="HomePageTemplate"> <div>This is some simple HTML</div> </t> </templates>
t-name采用被定义模板的名称,并声明可使用QWeb.render()来调用它。它只能在模板文件的顶层使用。
t-esc指令可用于输出文本:
<div>Hello <t t-esc="name"/></div>
它须要一个通过评估的Javascript表达式,而后表达式的结果被HTML转义并插入到文档中。因为它是一个表达式,所以能够像上面那样仅提供一个变量名称,或者像计算这样的更复杂的表达式:
<div><t t-esc="3+5"/></div>
或方法调用:
<div><t t-esc="name.toUpperCase()"/></div>
要在呈现的页面中注入HTML,请使用t-raw。像t-esc同样,它以一个任意的Javascript表达式做为参数,但它不执行HTML转义步骤。
<div><t t-raw="name.link('http://www.baidu.com')"/></div> <!-- 产生一个超连接,指向百度-->
t-raw不得用于用户提供的任何可能包含非转义内容的数据,由于这会致使跨站脚本漏洞。
QWeb可使用t-if的条件块。该指令采用任意表达式,若是表达式为falsy(false,null,0或空字符串),则整个块将被抑制,不然将显示该表达式。
<div> <t t-if="true == true"> true is true </t> <t t-if="true == false"> true is not true </t> </div>
QWeb没有“else”结构,若是原始条件反转,则使用第二个t。若是它是复杂或昂贵的表达式,您可能须要将条件存储在局部变量中。
要在列表上迭代,请使用t-foreach和t-as。 t-foreach须要一个表达式返回一个列表来迭代t - 由于在迭代过程当中须要一个变量名来绑定到每一个项目。
<div> <t t-foreach="names" t-as="name"> <div> Hello <t t-esc="name"/> </div> </t> </div>
t-foreach也能够用于数字和对象(字典)。
QWeb提供了两个相关的指令来定义计算属性:t-att-name和t-attf-name。不管哪一种状况,name都是要建立的属性的名称(例如t-att-id在渲染后定义属性id)。
t-att-接受一个javascript表达式,其结果被设置为属性的值,若是计算该属性的全部值,则它是很是有用的:
<div> Input your name: <input type="text" t-att-value="defaultName"/> </div>
t-attf-采用格式字符串。格式字符串是带有插值块的文本文本,插值块是{{和}}之间的javascript表达式,它将被表达式的结果替换。对于部分文字和部分计算的属性(如类),这是最有用的:
<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}"> insert content here </div>
模板能够拆分红子模板(为了简单,可维护性,可重用性或避免过多的标记嵌套)。
这是经过使用t-call指令完成的,该指令采用要呈现的模板的名称:
<t t-name="A"> <div class="i-am-a"> <t t-call="B"/> </div> </t> <t t-name="B"> <div class="i-am-b"/> </t>
渲染A模板将致使:
<div class="i-am-a"> <div class="i-am-b"/> </div>
子模板继承其调用者的渲染上下文。
在Widgets建立一个构件除了parent:product_names和color以外还有两个参数的构件。
odoo.oepetstore = function(instance, local) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; local.HomePage = instance.Widget.extend({ start: function() { var products = new local.ProductsWidget( this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00"); products.appendTo(this.$el); }, }); local.ProductsWidget = instance.Widget.extend({ template: "ProductsWidget", init: function(parent, products, color) { this._super(parent); this.products = products; this.color = color; }, }); instance.web.client_actions.add( 'petstore.homepage', 'instance.oepetstore.HomePage'); }
小部件的jQuery选择器
在窗口小部件中选择DOM元素能够经过调用窗口小部件的DOM根目录上的find()方法来执行:
this.$el.find("input.my_input")...
可是因为这是一种常见的操做,Widget()经过$()方法提供了一个等效的快捷方式:
local.MyWidget = instance.Widget.extend({ start: function() { this.$("input.my_input")... }, });
全局jQuery函数$()应该永远不会被使用(不是this.$()),除非它是绝对必要的:对一个小部件的根进行选择的范围是小部件,对本地来讲是本地的,可是使用$()的选择对于页面/应用程序是全局的,而且能够匹配部分其余小部件和视图,致使奇怪或危险的反作用。因为小部件一般只应用于其拥有的DOM部分,所以没有全局选择的缘由。
咱们之前使用常规jQuery事件处理程序(例如,.click()或.change())在窗口小部件元素上绑定了DOM事件:
local.MyWidget = instance.Widget.extend({ start: function() { var self = this; this.$(".my_button").click(function() { self.button_clicked(); }); }, button_clicked: function() { .. }, });
虽然这有效,但它有一些问题:
小部件所以提供了经过事件绑定DOM事件的捷径:
local.MyWidget = instance.Widget.extend({ events: { "click .my_button": "button_clicked", }, button_clicked: function() { .. } });
event 是事件触发时调用的函数或方法的对象(映射):
关键是一个事件名称,可能使用CSS选择器进行优化,在这种状况下,只有当事件发生在选定的子元素上时,函数或方法才会运行:点击将处理小部件内的全部点击,但单击.my_button将只处理点击含有my_button类的元素。
该值是触发事件时要执行的操做。
它也能够这样描述:
events: { 'click': function (e) { /* code here */ } }
或对象上方法的名称(请参见上面的示例)。
不管哪一种状况,这都是小部件实例,而且处理程序被赋予一个参数,即事件的jQuery事件对象。
小部件提供了一个事件系统(与上面描述的DOM / jQuery事件系统分开):一个小部件能够触发自身的事件,其余小部件(或其自己)能够绑定本身并监听这些事件:
local.ConfirmWidget = instance.Widget.extend({ events: { 'click button.ok_button': function () { this.trigger('user_chose', true); }, 'click button.cancel_button': function () { this.trigger('user_chose', false); } }, start: function() { this.$el.append("<div>Are you sure you want to perform this action?</div>" + "<button class='ok_button'>Ok</button>" + "<button class='cancel_button'>Cancel</button>"); }, });
trigger()将触发事件的名称做为其第一个(必需)参数,任何其余参数都视为事件数据并直接传递给侦听器。
而后,咱们能够设置一个父事件来实例化咱们的通用小部件,并使用on()来监听user_chose事件:
local.HomePage = instance.Widget.extend({ start: function() { var widget = new local.ConfirmWidget(this); widget.on("user_chose", this, this.user_chose); widget.appendTo(this.$el); }, user_chose: function(confirm) { if (confirm) { console.log("The user agreed to continue"); } else { console.log("The user refused to continue"); } }, });
on()绑定一个函数,当event_name标识的事件发生时被调用。 func参数是要调用的函数,object是该函数与方法相关的对象。绑定函数将被调用trigger()(若是有的话)的附加参数。例:
start: function() { var widget = ... widget.on("my_event", this, this.my_event_triggered); widget.trigger("my_event", 1, 2, 3); }, my_event_triggered: function(a, b, c) { console.log(a, b, c); // will print "1 2 3" }
提示:
触发其余小部件上的事件一般是一个坏主意。该规则的主要例外是odoo.web.bus,它专门用于广播任何小部件可能对整个Odoo Web应用程序感兴趣的平台。