本文檔介紹了Odoo Javascript框架。 這個框架在代碼行方面不是一個大型應用程序,但它很是通用,由於它基本上是一個將聲明性接口描述轉換爲實時應用程序的機器,可以與數據庫中的每一個模型和記錄進行交互。 甚至可使用Web客戶端來修改Web客戶端的界面。javascript
Odoo中全部文檔字符串的html版本可在如下位置得到:
Javascript APIcss
Javascript框架旨在處理三個主要用例:html
主要是有帳戶的用戶登陸 ,無帳戶數據瀏覽 , 銷售的接口java
簡而言之,WebClient的WebClient實例是整個用戶界面的根組件。 它的責任是協調全部各類子組件,並提供服務,如rpcs,本地存儲等。python
在運行時,Web客戶端是單頁面應用程序。 每次用戶執行操做時,都不須要從服務器請求完整頁面。 相反,它只會請求它所需的內容,而後替換/更新視圖。 此外,它管理URL:它與Web客戶端狀態保持同步。jquery
這意味着當用戶正在使用Odoo時,Web客戶端類(和操做管理器)實際上會建立並銷燬許多子組件。 狀態是高度動態的,每一個小部件均可以隨時銷燬。web
在這裏,咱們在web / static / src / js插件中快速概述了Web客戶端代碼。 請注意,故意不是詳盡無遺的。 咱們只覆蓋最重要的文件/文件夾。ajax
在Odoo中管理資產並不像在其餘一些應用程序中那樣簡單。 其中一個緣由是咱們有各類狀況須要一些但不是全部資產。 例如,Web客戶端,銷售點,網站甚至移動應用程序的需求是不一樣的。 此外,一些資產可能很大,但不多須要。 在這種狀況下,咱們有時但願它們被懶惰地加載。chrome
主要思想是咱們在xml中定義一組bundle。 捆綁包在此定義爲文件集合(javascript,css,less)。 在Odoo中,最重要的包在addons / web / views / webclient_templates.xml文件中定義。 它看起來像這樣:數據庫
<template id="web.assets_common" name="Common Assets (used in backend interface and website)"> <link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/> ... <script type="text/javascript" src="/web/static/src/js/boot.js"></script> ... </template>
而後可使用t-call-assets指令將包中的文件插入到模板中:
<t t-call-assets="web.assets_common" t-js="false"/> <t t-call-assets="web.assets_common" t-css="false"/>
如下是服務器使用這些指令呈現模板時發生的狀況:
捆綁包中描述的全部較少的文件都被編譯成css文件。 名爲file.less的文件將在名爲file.less.css的文件中編譯。
if we are in debug=assets mode,
將t-js屬性設置爲false的t-call-assets指令將替換爲指向css文件的樣式表標記列表
將t-css屬性設置爲false的t-call-assets指令將替換爲指向js文件的腳本標記列表
if we are not in debug=assets mode,
css文件將被鏈接和縮小,而後分紅不超過4096條規則的文件(以克服IE9的舊限制)。 而後,咱們根據須要生成儘量多的樣式表標籤
將js文件鏈接並縮小,而後生成腳本標記
請注意,資產文件是緩存的,所以理論上,瀏覽器只應加載一次。
啓動Odoo服務器時,它會檢查捆綁包中每一個文件的時間戳,若有必要,將建立/從新建立相應的捆綁包。
如下是大多數開發人員須要瞭解的一些重要捆綁包:
web.assets_common:此捆綁包包含Web客戶端,網站以及銷售點通用的大多數資產。 這應該包含odoo框架的低級構建塊。 請注意,它包含boot.js文件,該文件定義了odoo模塊系統。
web.assets_backend:此捆綁包含特定於Web客戶端的代碼(特別是Web客戶端/操做管理器/視圖)
web.assets_frontend:這個包是全部特定於公共網站的:電子商務,論壇,博客,活動管理,......
將位於addons / web中的文件添加到包中的正確方法很簡單:只需將文件webclient_templates.xml中的腳本或樣式表標記添加到包中便可。 可是當咱們在不一樣的插件中工做時,咱們須要從該插件添加一個文件。 在這種狀況下,應該分三步完成:
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend"> <xpath expr="//script[last()]" position="after"> <link rel="stylesheet" href="/helpdesk/static/src/less/helpdesk.less"/> <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script> </xpath> </template>
請注意,當用戶加載odoo Web客戶端時,捆綁包中的文件都會當即加載。 這意味着每次都經過網絡傳輸文件(瀏覽器緩存處於活動狀態時除外)。 在某些狀況下,延遲加載某些資產可能更好。 例如,若是窗口小部件須要大型庫,而且該窗口小部件不是體驗的核心部分,那麼在實際建立窗口小部件時僅加載庫多是個好主意。 widget類實際上內置了對此用例的支持。 (參見QWeb模板引擎部分)
文件可能沒法正確加載的緣由有不少。 如下是您能夠嘗試解決此問題的一些事項:
從新建立資產文件後,您須要刷新頁面,從新加載正確的文件(若是不起做用,能夠緩存文件)。
一旦咱們可以將javascript文件加載到瀏覽器中,咱們須要確保它們以正確的順序加載。 爲了作到這一點,Odoo定義了一個小模塊系統(位於addons / web / static / src / js / boot.js文件中,須要先加載)。
受AMD啓發的Odoo模塊系統經過在全局odoo對象上定義函數define來工做。 而後咱們經過調用該函數來定義每一個javascript模塊。 在Odoo框架中,模塊是一段將盡快執行的代碼。 它有一個名稱,可能還有一些依賴項。 加載其依賴項後,也會加載一個模塊。 而後,模塊的值是定義模塊的函數的返回值。
例如,它可能以下所示:
// in file a.js odoo.define('module.A', function (require) { "use strict"; var A = ...; return A; }); // in file b.js odoo.define('module.B', function (require) { "use strict"; var A = require('module.A'); var B = ...; // something that involves A return B; });
定義模塊的另外一種方法是在第二個參數中明確給出依賴項列表。
odoo.define('module.Something', ['module.A', 'module.B'], function (require) { "use strict"; var A = require('module.A'); var B = require('module.B'); // some code });
若是某些依賴項缺失/未就緒,則不會加載該模塊。 幾秒鐘後控制檯會出現警告。
請注意,不支持循環依賴項。 這是有道理的,但這意味着須要當心。
odoo.define方法有三個參數:
moduleName:javascript模塊的名稱。它應該是一個獨特的字符串。慣例是使用odoo插件的名稱,而後是特定的描述。例如,'web.Widget'描述了在web插件中定義的模塊,該模塊導出Widget類(由於第一個字母是大寫的)
若是名稱不惟一,則會拋出異常並在控制檯中顯示。
最後,最後一個參數是一個定義模塊的函數。它的返回值是模塊的值,能夠傳遞給須要它的其餘模塊。請注意,異步模塊有一個小例外,請參閱下一節。
若是發生錯誤,將在控制檯中記錄(在調試模式下):
Missing dependencies
:這些模塊不會出如今頁面中。 JavaScript文件可能不在頁面中或模塊名稱錯誤Failed modules
:檢測到javascript錯誤Rejected modules
:該模塊返回被拒絕的延遲。 它(及其相關模塊)未加載。Rejected linked modules
:依賴被拒絕模塊的模塊Non loaded modules
:依賴於丟失或失敗模塊的模塊模塊在準備好以前須要執行一些工做。 例如,它能夠執行rpc來加載一些數據。 在這種狀況下,模塊能夠簡單地返回延遲(promise)。 在這種狀況下,模塊系統將在註冊模塊以前等待延遲完成。
odoo.define('module.Something', ['web.ajax'], function (require) { "use strict"; var ajax = require('web.ajax'); return ajax.rpc(...).then(function (result) { // some code here return something; }); });
web.dom_ready
模塊返回一個deferred,當dom實際就緒時將解析。 所以,須要DOM的另外一個模塊可能只是在某處有一個require('web.dom_ready')
語句,而代碼只會在DOM準備就緒時執行。Odoo是在ECMAScript 6課程開始以前開發的。 在Ecmascript 5中,定義類的標準方法是定義一個函數並在其原型對象上添加方法。 這很好,可是當咱們想要使用繼承,mixins時,它有點複雜。
出於這些緣由,Odoo決定使用本身的類系統,靈感來自John Resig。 基類位於web.Class中,位於文件class.js中。
讓咱們討論如何建立類。 主要機制是使用extend方法(這或多或少至關於ES6類中的extend)。
var Class = require('web.Class'); var Animal = Class.extend({ init: function () { this.x = 0; this.hunger = 0; }, move: function () { this.x = this.x + 1; this.hunger = this.hunger + 1; }, eat: function () { this.hunger = 0; }, });
在此示例中,_init_函數是構造函數。 它將在建立實例時調用。 使用new關鍵字完成實例。
可以繼承現有類很方便。 這能夠經過在超類上使用extend方法來完成。 調用方法時,框架將祕密從新綁定一個特殊方法:_super到當前調用的方法。 這容許咱們在須要調用父方法時使用this._super。
var Animal = require('web.Animal'); var Dog = Animal.extend({ move: function () { this.bark(); this._super.apply(this, arguments); }, bark: function () { console.log('woof'); }, }); var dog = new Dog(); dog.move()
odoo類系統不支持多重繼承,可是對於那些咱們須要共享某些行爲的狀況,咱們有一個mixin系統:extend方法實際上能夠接受任意數量的參數,並將全部這些參數組合在新類中。
var Animal = require('web.Animal'); var DanceMixin = { dance: function () { console.log('dancing...'); }, }; var Hamster = Animal.extend(DanceMixin, { sleep: function () { console.log('sleeping'); }, });
在這個例子中,Hamster類是Animal的子類,但它也混合了DanceMixin。
這並不常見,但咱們有時須要修改另外一個類。 目標是有一個機制來改變一個類和全部將來/如今的實例。 這是經過使用include方法完成的:
var Hamster = require('web.Hamster'); Hamster.include({ sleep: function () { this._super.apply(this, arguments); console.log('zzzz'); }, });
Widget類其實是用戶界面的重要構建塊。 幾乎用戶界面中的全部內容都在窗口小部件的控制之下。 Widget類在widget.js中的模塊web.Widget中定義。
簡而言之,Widget類提供的功能包括:
如下是基本計數器小部件的示例:
var Widget = require('web.Widget'); var Counter = Widget.extend({ template: 'some.template', events: { 'click button': '_onClick', }, init: function (parent, value) { this._super(parent); this.count = value; }, _onClick: function () { this.count++; this.$('.val').text(this.count); }, });
對於此示例,假設模板some.template(而且已正確加載:模板位於文件中,該模板在模塊清單中的qweb鍵中正肯定義)由下式給出:
<div t-name="some.template"> <span class="val"><t t-esc="widget.count"/></span> <button>Increment</button> </div>
此示例窗口小部件能夠按如下方式使用:
// Create the instance var counter = new Counter(this, 4); // Render and insert into DOM counter.appendTo(".some-div");
此示例說明了Widget類的一些功能,包括事件系統,模板系統,具備初始父參數的構造函數。
與許多組件系統同樣,widget類具備明肯定義的生命週期。 一般的生命週期以下:調用init,而後啓動,而後進行渲染,而後啓動並最終銷燬。
Widget.init(parent)
這是構造函數。 init方法應該初始化小部件的基本狀態。 它是同步的,能夠被覆蓋以從小部件的建立者/父級中獲取更多參數
Arguments
parent (Widget()
)-- 新窗口小部件的父窗口,用於處理自動銷燬和事件傳播。 對於沒有父項的窗口小部件,能夠爲null
。
Widget.willStart()
在建立窗口小部件時以及在附加到DOM的過程當中,此方法將由框架調用一次。 willStart方法是一個應該返回延遲的鉤子。 在繼續渲染步驟以前,JS框架將等待延遲完成。 請注意,此時,窗口小部件沒有DOM根元素。 willStart鉤子對於執行某些異步工做(例如從服務器獲取數據)很是有用
[Rendering]()
此步驟由框架自動完成。 會發生什麼是框架檢查是否在窗口小部件上定義了模板鍵。 若是是這種狀況,那麼它將使用綁定到渲染上下文中的窗口小部件的窗口小部件鍵來呈現該模板(請參閱上面的示例:咱們在QWeb模板中使用widget.count來讀取窗口小部件中的值)。 若是沒有定義模板,咱們讀取tagName鍵並建立相應的DOM元素。 渲染完成後,咱們將結果設置爲窗口小部件的$ el屬性。 在此以後,咱們會自動綁定events和custom_events鍵中的全部事件。
Widget.start()
渲染完成後,框架將自動調用start方法。 這對於執行一些專門的後期渲染工做頗有用。 例如,設置庫。
必須返回延遲以指示其工做什麼時候完成。
Returns: deferred object
Widget.destroy()
這始終是小部件生命中的最後一步。 當一個小部件被銷燬時,咱們基本上執行全部必要的清理操做:從組件樹中刪除小部件,解除全部事件的綁定,......
當窗口小部件的父窗體被銷燬時自動調用,若是窗口小部件沒有父窗口,或者若是它被刪除但其父窗口仍然存在,則必須顯式調用。
請注意,不必定要調用willStart和start方法。 能夠建立一個小部件(將調用init方法),而後銷燬(destroy方法),而沒必要將其附加到DOM。 若是是這種狀況,則甚至不會調用willStart和start。
Widget.tagName
若是窗口小部件未定義模板,則使用 默認爲div,將用做標記名稱以建立DOM元素以設置爲窗口小部件的DOM根。 可使用如下屬性進一步自定義今生成的DOM根:
Widget.id
用於在生成的DOM根上生成id屬性。 請注意,這不多須要,若是窗口小部件能夠屢次使用,可能不是一個好主意。
Widget.className
用於在生成的DOM根上生成類屬性。 請注意,它實際上能夠包含多個css類:'some-class other-class'
Widget.attributes
將屬性名稱(對象文字)映射到屬性值。 這些k:v對中的每個將被設置爲生成的DOM根上的DOM屬性。
Widget.el
原始DOM元素設置爲窗口小部件的根(僅在啓動生命週期方法以後可用)
Widget.template
應設置爲QWeb模板的名稱。 若是設置,模板將在窗口小部件初始化以後但在啓動以前呈現。 模板生成的根元素將設置爲窗口小部件的DOM根。
xmlDependencies
在呈現窗口小部件以前須要加載的xml文件的路徑列表。 這不會致使加載任何已加載的東西。
events
事件是事件選擇器(事件名稱和由空格分隔的可選CSS選擇器)到回調的映射。 回調能夠是窗口小部件方法或函數對象的名稱。 在任何一種狀況下,this
都將設置爲小部件:
events: { 'click p.oe_some_class a': 'some_method', 'change input': function (e) { e.stopPropagation(); } },
選擇器用於jQuery的事件委託,只有與選擇器匹配的DOM根的後代纔會觸發回調。 若是省略了選擇器(僅指定了事件名稱),則將直接在窗口小部件的DOM根上設置事件。
注意:不鼓勵使用內聯函數,未來有時可能會刪除它。
custom_events
這幾乎與events屬性相同,但鍵是任意字符串。 它們表示由某些子窗口小部件觸發的業務事件。 當一個事件被觸發時,它將「冒泡」小部件樹(有關更多詳細信息,請參閱有關組件通訊的部分)。
Widget.isDestroyed()
Returns:
true
若是小部件正在被銷燬或被銷燬,不然爲假
Widget.$(selector)
將指定爲參數的CSS選擇器應用於窗口小部件的DOM根目錄:
this.$(selector);
在功能上與:
this.$el.find(selector);
Arguments:
selector (String) -- CSS selector
Returns: jQuery object
這個幫助方法相似於Backbone.View.$
Widget.setElement(element)
將窗口小部件的DOM根從新設置爲提供的元素,還處理從新設置DOM根的各類別名以及取消設置和從新設置委派事件。
Arguments:
element (Element) -- a DOM element or jQuery object to set as the widget's DOM root 要設置爲窗口小部件DOM根的DOM元素或jQuery對象
Widget.appendTo(element)
呈現窗口小部件並將其做爲目標的最後一個子項插入,使用.appendTo()
Widget.prependTo(element)
呈現窗口小部件並將其做爲目標的第一個子項插入,使用.prependTo()
Widget.insertAfter(element)
渲染窗口小部件並將其做爲目標的前一個兄弟插入,使用.insertAfter()
Widget.insertBefore(element)
渲染窗口小部件並將其做爲目標的如下兄弟插入,使用.insertBefore()
全部這些方法都接受相應的jQuery方法接受的任何內容(CSS選擇器,DOM節點或jQuery對象)。 他們都返回延期,並負責三項任務:
rendering the widget's root element via(渲染窗口小部件的根元素經過)
renderElement()
啓動窗口小部件,並返回啓動它的結果
id
限制了組件的可重用性,而且每每使代碼更脆弱。 大多數狀況下,它們能夠替換爲任何內容,類或保持對DOM節點或jQuery元素的引用。若是id
是絕對必要的(由於第三方庫須要一個),則應使用_.uniqueId()
部分生成id,例如:
this.id = _.uniqueId('my-widget-');
避免使用可預測/常見的CSS類名。 諸如「內容」或「導航」之類的類名稱可能與所需的含義/語義相匹配,但極可能其餘開發人員具備相同的需求,從而產生命名衝突和意外行爲。 通用類名稱應以例如前綴爲例。 它們所屬組件的名稱(建立「非正式」命名空間,就像在C或Objective-C中同樣)。
應避免使用全局選擇器。 因爲組件可能在單個頁面中屢次使用(Odoo中的示例是儀表板),所以查詢應限制在給定組件的範圍內。 未過濾的選擇(例如$(selector)
或document.querySelectorAll(selector)
一般會致使意外或不正確的行爲。 Odoo Web的Widget()
具備提供其DOM根($ el
)的屬性,以及直接選擇節點的快捷方式($()
)。
更通常地說,永遠不要假設您的組件擁有或控制超出其我的$ el
的任何內容(所以,避免使用對父窗口小部件的引用)
除非絕對瑣碎,不然Html模板/渲染應該使用QWeb。
全部交互式組件(向屏幕顯示信息或攔截DOM事件的組件)必須從Widget()
繼承並正確實現和使用其API和生命週期。
Web客戶端使用QWeb模板引擎來呈現窗口小部件(除非它們覆蓋renderElement方法以執行其餘操做)。 Qweb JS模板引擎基於XML,而且主要與python實現兼容。
如今,讓咱們解釋一下如何加載模板。 每當Web客戶端啓動時,都會對/ web / webclient / qweb路由創建一個rpc。 而後,服務器將返回每一個已安裝模塊的數據文件中定義的全部模板的列表。 每一個模塊清單中的qweb條目中都列出了正確的文件。
在啓動第一個小部件以前,Web客戶端將等待加載該模板列表。
這種機制能夠很好地知足咱們的需求,但有時候,咱們想要延遲加載模板。 例如,假設咱們有一個不多使用的小部件。 在這種狀況下,咱們可能不但願在主文件中加載其模板,以使Web客戶端稍微輕一些。 在這種狀況下,咱們可使用Widget的xmlDependencies鍵:
var Widget = require('web.Widget'); var Counter = Widget.extend({ template: 'some.template', xmlDependencies: ['/myaddon/path/to/my/file.xml'], ... });
有了這個,Counter小部件將在其willStart方法中加載xmlDependencies文件,所以在執行渲染時模板將準備就緒。
目前Odoo支持兩種事件系統:一個容許添加監聽器和觸發事件的簡單系統,以及一個更完整的系統,它也會使事件「冒泡」。
這兩個事件系統都在事件mixins.js中的EventDispatcherMixin中實現。 這個mixin包含在Widget類中。
這個事件系統在歷史上是第一個。 它實現了一個簡單的總線模式。 咱們有4種主要方法:
如下是有關如何使用此事件系統的示例:
var Widget = require('web.Widget'); var Counter = require('myModule.Counter'); var MyWidget = Widget.extend({ start: function () { this.counter = new Counter(this); this.counter.on('valuechange', this, this._onValueChange); var def = this.counter.appendTo(this.$el); return $.when(def, this._super.apply(this, arguments); }, _onValueChange: function (val) { // do something with val }, }); // in Counter widget, we need to call the trigger method: ... this.trigger('valuechange', someValue);