構建接口擴展(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應用程序感興趣的平臺。