odoo javascript

本文介紹了odoo javascript框架。從代碼行的角度來看,這個框架不是一個大的應用程序,但它是很是通用的,由於它基本上是一個將聲明性接口描述轉換爲活動應用程序的機器,可以與數據庫中的每一個模型和記錄交互。甚至可使用Web客戶端修改Web客戶端的接口。javascript

這裏有一個有用的html版本的文檔:Javascript APIcss

概覽

這個Javascript框架主要設計用於三個地方使用:html

  • web客戶端:這是一個私有的web應用,能夠在其中查看和編輯業務數據。這是一個單頁應用程序(永遠不會從新加載該頁,只在須要時從服務器提取新數據)。
  • 網站:這是Odoo的公共部分。它容許身份不明的用戶做爲客戶端瀏覽某些內容、購物或執行許多操做。這是一個經典的網站:各類各樣的帶有控制器的路由和共同協做的Javascript代碼。
  • POS:這是銷售點的接口。它是一個特定的但也應用程序。

Web客戶端

單頁應用

簡而言之,webclient實例是整個用戶界面的根組件。它的職責是協調全部的子組件,並提供服務,如RPC、本地存儲等等。java

在運行時,Web客戶端是單頁應用程序。每次用戶執行操做時,它不須要從服務器請求整頁。相反,它只請求它所須要的,而後替換/更新視圖。此外,它還管理URL:它與Web客戶機狀態保持同步。python

這意味着,當用戶在處理odoo時,Web客戶機類(和動做管理器)實際上建立並銷燬了許多子組件。狀態是高度動態的,每一個小部件均可以隨時銷燬。jquery

Web客戶端JS代碼概覽

這裏,咱們在web/static/src/js插件中快速概述了web客戶機代碼。注意,這是故意不詳盡的,咱們只涉及最重要的文件/文件夾。web

  • boot.js : 這是定義模塊系統的文件,它須要首先加載。
  • core/ : 這是較低級別的構建基塊的集合。值得注意的是,它包含類系統、小部件系統、併發實用程序和許多其餘類/函數。
  • chorm/ :在這個文件夾中,咱們有大多數大的小部件,它們構成了大部分用戶界面。
  • chrome/abstract_web_client.js and chrome/web_client.js : 這些文件一塊兒定義了WebClient小部件(widget),它是Web客戶機的根小部件(wideget)。
  • chrome/action_manager.js : 這是將動做(action)轉換爲小部件(widget)(例如看板或表單視圖)的代碼。
  • chrome/search_X.js : 全部這些文件定義了搜索視圖(它不是Web客戶機視圖中的視圖,僅從服務器視圖)
  • fields : 這裏定義了全部主要字段視圖小部件(widget)
  • views : 這是視圖所在的位置

資源管理

在Odoo中管理資源並不像在其餘應用程序中那樣簡單。其中一個緣由是,在其中一些狀況中咱們有各類各樣的狀態,但不是全部的資源都是必需的。例如,Web客戶端、銷售點、網站甚至移動應用程序的需求是不一樣的。此外,有些資源可能很大,但不多須要。在這種狀況下,咱們有時但願它們被懶惰地加載。ajax

主要思想是咱們用XML定義一組包。捆綁包在這裏定義爲一組文件(javascript、css、scss)。在odoo中,最重要的包在addons/web/views/webclient_templates.xml文件中定義。看起來是這樣的:chrome

<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"/> 

下面是當服務器使用如下指令呈現模板時發生的狀況:

  • 包中描述的全部SCSS文件都編譯爲CSS文件。名爲file.scss的文件將編譯在名爲file.scss.css的文件中。
  • 若是咱們在debug=assets模式
       * t-js屬性設置爲false的t-call-assets指令將替換爲指向css文件的樣式表標記列表。
       * t-css屬性設置爲false的t-call-assets指令將替換爲指向JS文件的腳本標記列表。
  • 若是咱們不在debug=assets模式
       * CSS文件將被鏈接並縮小,而後拆分爲不超過4096個規則的文件(以繞過IE9的舊限制)。而後,咱們根據須要生成儘量多的樣式表標籤
       * JS文件被鏈接並縮小,而後生成一個腳本標記。

請注意,資源文件是緩存的,所以從理論上講,瀏覽器應該只加載它們一次。

主包

當odoo服務器啓動時,它檢查包中每一個文件的時間戳,若是須要,它將建立/從新建立相應的包。

如下是大多數開發人員須要知道的一些重要包:

  • web.assets_common : 此包包含Web客戶端、網站以及銷售點(POS)所共有的大多數資源。這應該包含用於Odoo框架的較低級別的構建塊。注意,它包含boot.js文件,它定義了odoo模塊系統。
  • web.assets_backend :這個包包含特定於Web客戶端的代碼(特別是Web客戶端/動做管理器/視圖)
  • web.assets_frontend :這個包是關於全部特定於公共網站的:電子商務、論壇、博客、事件管理…

在一個資源包裏添加文件

將位於addons/web中的文件添加到bundle的正確方法很簡單:只需將腳本或樣式表標記添加到文件webclient_templates.xml中的bundle便可。可是當咱們使用不一樣的插件(addon)時,咱們須要從該插件添加一個文件。在這種狀況下,應分三步進行:

  1. 添加一個 assets.xml 文件到views/文件夾
  2. 添加字符'views/assets.xml' 到manifest文件的鍵'data'的值裏
  3. 建立所需包的繼承視圖,並使用xpath表達式添加文件。例如:
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend"> <xpath expr="//script[last()]" position="after"> <link rel="stylesheet" type="text/scss" href="/helpdesk/static/src/scss/helpdesk.scss"/> <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script> </xpath> </template> 

請注意,當用戶加載odoo web客戶端時,包中的全部文件都會當即加載。這意味着每次經過網絡傳輸文件(瀏覽器緩存處於活動狀態時除外)。在某些狀況下,最好使用Lazyload的一些資產。例如,若是一個小部件須要一個大的庫,而這個小部件不是體驗的核心部分,那麼在實際建立小部件時,最好只加載庫。widget類實際上已經爲這個用例內置了支持。(查閱QWeb模板引擎部分)

若是文件沒有加載/更新應該怎麼辦

文件可能沒法正確加載有許多不一樣的緣由。您能夠嘗試如下幾點來解決此問題:

  • 一旦服務器啓動,它就不知道資源文件是否已被修改。所以,您能夠簡單地從新啓動服務器來從新生成資源。
  • 檢查控制檯(在開發工具中,一般用F12打開),確保沒有明顯的錯誤
  • 嘗試在文件的開頭添加console.log(在任何模塊定義以前),這樣您就能夠查看文件是否已加載。
  • 在用戶界面中,在調試模式下(在此處插入連接到調試模式),有一個選項能夠強制服務器更新其資源文件。
  • 使用debug=assets模式。這實際上會繞過資源包(請注意,它實際上並不能解決問題,服務器仍然使用過期的包)
  • 最後,對於開發人員來講,最方便的方法是使用--dev=all選項啓動服務器。這將激活文件監視程序選項,必要時將自動使資源無效。請注意,若是操做系統是Windows,它就不能很好地工做。
  • 記住刷新頁面!
  • 或者保存代碼文件…

從新建立資源文件後,須要刷新頁面,從新加載正確的文件(若是不起做用,文件可能被緩存了)。

Javascript模塊系統

一旦咱們可以將咱們的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插件(addon)的名字後面加上一個具體的描述。例如,「web.widget」描述在web插件中定義的模塊,該模塊導出一個widget類(由於第一個字母大寫)。
    若是名稱不惟一,將引起異常並顯示在控制檯中
  • dependencies : 第二個參數是可選的。若是給定,它應該是一個字符串列表,每一個字符串對應一個JavaScript模塊。這描述了在執行模塊以前須要加載的依賴項。若是這裏沒有明確地給出依賴項,那麼模塊系統將經過調用ToString從函數中提取它們,而後使用regexp查找全部Require語句。
  • 最後一個參數是定義模塊的函數。它的返回值是模塊的值,能夠傳遞給其餘須要它的模塊。注意,異步模塊有一個小的異常,請參見下一節。
    若是發生錯誤,將在控制檯中記錄(在調試模式下):
  • Missing dependencies: 這些模塊不會出如今頁面中。多是javascript文件不在頁面中或模塊名稱錯誤
  • Failed Modules : 一個Javascript錯誤被檢測到
  • Rejected modules :塊返回拒絕的延遲。它(及其相關模塊)未加載。
  • Rejected linked modules: 依賴被拒絕模塊的模塊
  • Non loaded modules : 模塊依賴了一個缺失/失敗的模塊

異步模塊

模塊可能須要在準備就緒以前執行一些工做。例如,它能夠作一個RPC來加載一些數據。在這種狀況下,模塊只需返回一個deferred(promise)。在這種狀況下,模塊系統只需等待deferred完成,而後註冊模塊。

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實際就緒時,這個deferred 將被解決。所以,另外一個須要dom的模塊能夠在某個地方簡單地有一個require(「web.dom_ready」)語句,而且只有當dom準備好時纔會執行代碼。
  • 儘可能避免在一個文件中定義多個模塊。這在短時間內可能很方便,但實際上很難維護。

類系統

Odoo是在ECMAScript 6類可用以前開發的。在ECMAScript 5中,定義類的標準方法是定義一個函數並在其原型對象上添加方法。這很好,可是當咱們想要使用繼承、混合時,它稍微複雜一些。
出於這些緣由,Odoo決定使用本身的類系統,這是受到約翰·雷西格的啓發。基類位於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類系統不支持多重繼承,可是對於那些須要共享某些行爲的狀況,咱們有一個混合系統:extend方法實際上能夠接受任意數量的參數,並將它們組合到新的類中。

var Animal = require('web.Animal'); var DanceMixin = { dance: function () { console.log('dancing...'); }, }; var Hamster = Animal.extend(DanceMixin, { sleep: function () { console.log('sleeping'); }, }); 

在這個例子中,Hamter 類是Animal的子類,可是它也混合了DanceMixin.

修改現有的類

這並不常見,但有時咱們須要在適當的位置修改另外一個類。目標是有一個機制來改變一個類和全部將來/如今的實例。這是經過使用include方法完成的:

var Hamster = require('web.Hamster'); Hamster.include({ sleep: function () { this._super.apply(this, arguments); console.log('zzzz'); }, }); 

這顯然是一個危險的操做,應該當心操做。可是,按照odoo的結構,有時須要在一個插件中修改在另外一個插件中定義的widget/class的行爲。請注意,它將修改類的全部實例,即便它們已經建立。

小部件(Widget)

widget類其實是用戶界面的一個重要構建塊。幾乎用戶界面中的全部內容都在小部件(widget)的控制之下。widget類在widget.js中的module web.widget中定義。
簡而言之,widget類提供的特性包括:

  • 小部件之間的父/子關係(PropertiesMixin)
  • **具備安全功能的普遍生命週期管理 **(e.g. 在銷燬父級期間自動銷燬子窗口小部件)
  • 自動渲染qweb模板
  • 幫助與外部環境交互的各類實用功能。
    一個計數的小部件例子:
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> 

這個例子說明了小部件類的一些特性,包括事件系統、模板系統、帶有初始父參數的構造函數。

小部件的生命週期

與許多組件系統同樣,widget類有一個定義良好的生命週期。一般的生命週期以下:調用init,而後willStart,而後rendering,而後start,最後destroy。

Widget.init(parent)
這是構造函數。init方法應該初始化小部件的基本狀態。它是同步的,能夠被重寫以從小部件的建立者/父對象獲取更多參數。

Arguments : parent(Widget())–新的widget的父級,用於處理自動銷燬和事件傳播。對於沒有父級的小部件,能夠爲null

Widget.willStart()
當一個小部件被建立並被附加到DOM的過程當中,框架將調用這個方法一次。willstart方法是一個鉤子,它應該返回一個deferred。JS框架將等待這個deferred完成,而後再繼續渲染步驟。注意,此時小部件沒有dom根元素。willstart鉤子主要用於執行一些異步工做,例如從服務器獲取數據。

[Rendering]()
此步驟由框架自動完成。框架會檢查小部件上是否認義了template鍵。若是定義了,那麼它將在呈現上下文中使用綁定到小部件的widget鍵呈現該模板(請參見上面的示例:咱們在QWeb模板中使用widget.count來讀取小部件的值)。若是沒有定義模板,則讀取 tagName 鍵並建立相應的DOM元素。渲染完成後,咱們將結果設置爲小部件的$el屬性。在此以後,咱們將自動綁定events和custom_events鍵中的全部事件。

Widget.start()
渲染完成後,框架將自動調用Start方法。這對於執行一些特殊的後期渲染工做頗有用。例如,設置庫。
必須返回deferred以指示其工做什麼時候完成。

Returns: deferred 對象

Widget.destroy()
這始終是小部件生命週期中的最後一步。當小部件被銷燬時,咱們基本上執行全部必要的清理操做:從組件樹中刪除小部件,取消綁定全部事件,…
當小部件的父級被銷燬時自動調用,若是小部件沒有父級,或者若是它被刪除但父級仍然存在,則必須顯式調用。

請注意,沒必要調用willstart和start方法。能夠建立一個小部件(將調用init方法),而後銷燬(destroy方法),而不須要附加到DOM。若是是這種狀況,將不會調用will start和start。

Widget API

Widget.tagName
若是小部件沒有定義模板,則使用。默認爲DIV,將用做標記名來建立要設置爲小部件的dom根的dom元素。可使用如下屬性進一步自定義生成的dom根目錄:

Widget.id
用於在生成的dom根上生成id屬性。請注意,這是不多須要的,若是一個小部件能夠屢次使用,這可能不是一個好主意。

Widget.className
用於在生成的dom根上生成class屬性。請注意,它實際上能夠包含多個css類:「some-class other-class」

Widget.attributes
屬性名到屬性值的映射(對象文本)。這些k:v對中的每個都將被設置爲生成的dom根上的dom屬性。

Widget.el
將原始dom元素設置爲小部件的根(僅在start lifecyle方法以後可用)

Widget.$el
jquery封裝的el,(僅在Start Lifecyle方法以後可用)

Widget.template
應設置爲QWeb模板的名稱。若是設置了,模板將在小部件初始化以後但在其啓動以前呈現。模板生成的根元素將被設置爲小部件的dom根。

Widget.xmlDependencies
呈現小部件以前須要加載的XML文件的路徑列表。這不會致使加載已加載的任何內容。若是您想延遲加載模板,或者想要在網站和Web客戶機界面之間共享一個小部件,這頗有用。

var EditorMenuBar = Widget.extend({ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'], ... 

Widget.events
事件是事件選擇器(由空格分隔的事件名稱和可選CSS選擇器)到回調的映射。回調能夠是小部件方法或函數對象的名稱。在這兩種狀況下,這都將設置爲小部件:

'click p.oe_some_class a': 'some_method', 'change input': function (e) { e.stopPropagation(); } }, 

選擇器用於jquery的事件委託,回調只對與選擇器匹配的dom根的後代觸發。若是選擇器被省略(只指定了一個事件名),那麼事件將直接設置在小部件的dom根上。
注意:不鼓勵使用內聯函數,未來可能會刪除它。

Widget.custom_events

returns: true 若是小部件正在或者已經被銷燬,不然false

Widget.$(selector)
將指定爲參數的CSS選擇器應用於小部件的dom根:
this.$(selector);
功能上與如下相同:
this.$el.find(selector);

arguments: selector(string)-CSS選擇器
return:jQuery 對象

這個助手方法相似於Backbone.View.$

Widget.setElement(element)
將小部件的dom根從新設置爲提供的元素,還處理從新設置dom根的各類別名以及取消設置和從新設置委託事件。

arguments: element(Element) -一個DOM元素或者jQuery對象設置爲小部件的根DOM

在DOM中插入一個小部件

Widget.appendTo(element)
渲染小部件並將它做爲子元素插入到目標元素後面,使用.appentTo()

Widget.prependTo(element)
渲染小部件並將它做爲子元素插入到目標元素前面,使用.prependTo()

Widget.insertAfter(element)
渲染小部件並將它做爲目標元素的前一個同級插入,使用.insertAfter()

Widget.insertBefore(element)
渲染小部件並將其做爲目標的後一個同級插入,使用.insertBefore()

全部這些方法都接受相應jquery方法接受的任何內容(css選擇器、dom節點或jquery對象)。他們都會返回一個 deferred,並承擔三個任務:

  • 經過如下方式呈現小部件的根元素:
    renderElement()
  • 使用jquery在DOM中插入小部件的根元素
    匹配的方法
  • 啓動小部件並返回啓動結果

小部件指南

  • 在通常應用和模塊中中,標識 (id 屬性)應該避免使用,ID限制了組件的可重用性,並使代碼更加脆弱。大多數狀況下,它們能夠替換爲Nothing、Classes或保留對dom節點或jquery元素的引用。
    若是ID是絕對必要的(由於第三方庫須要一個),則應使用_.uniqueId()部分生成ID,例如:
    this.id = _.uniqueId('my-widget-');
  • 避免使用可預測/通用的CSS類名。類名稱(如「content」或「navigation」)可能與所需的含義/語義匹配,但其餘開發人員可能也有相同的需求,從而形成命名衝突和意外行爲。通用類名的前綴應該是它們所屬組件的名稱(建立「非正式」名稱空間,就像在C或Objective-C中那樣)。
  • 應避免使用全局選擇器。由於一個組件能夠在一個頁面中屢次使用(ODoo中的一個例子是儀表板),因此查詢應該限制在給定組件的範圍內。未篩選的選擇(如$(selector)document.querySelectorAll(selector))一般會致使意外或錯誤的行爲。odoo web的widget()有一個提供dom根(el)的屬性,以及直接選擇節點的快捷方式(_().)
  • 更通常地說,不要假設您的組件擁有或控制任何超出其我的$el的東西(所以,避免使用對父部件的引用)。
  • HTML模板/渲染應該使用QWeb,除非很是簡單。
  • 全部交互組件(向屏幕顯示信息或截取DOM事件的組件)必須繼承自widget(),並正確實現和使用其API和生命週期。

Qweb模板引擎

Web客戶端使用QWeb模板引擎來呈現小部件(除非它們重寫renderelement方法來執行其餘操做)。QWebJS模板引擎基於XML,主要與Python實現兼容。

如今,讓咱們解釋如何加載模板。每當Web客戶端啓動時,都會對/web/web client/qweb路由進行RPC。而後,服務器將返回在每一個已安裝模塊的數據文件中定義的全部模板的列表。正確的文件列在每一個模塊清單的QWeb條目中。

在啓動第一個小部件以前,Web客戶機將等待加載該模板列表。

這個機制能夠很好地知足咱們的需求,但有時咱們但願懶加載模板。例如,假設咱們有一個不多使用的小部件。在這種狀況下,咱們可能不但願將其模板加載到主文件中,以便使Web客戶機稍微輕一些。在這種狀況下,咱們可使用小部件的xmlpendencies鍵:

var Widget = require('web.Widget'); var Counter = Widget.extend({ template: 'some.template', xmlDependencies: ['/myaddon/path/to/my/file.xml'], ... }); 

有了這個,計數器小部件將以willstart方法加載xmlpendencies文件,這樣在執行呈現時模板就能夠準備好了。

事件系統

目前,odoo支持兩個事件系統:一個容許添加偵聽器和觸發事件的簡單系統,以及一個更完整的系統,它還可使事件「冒泡」。

這兩個事件系統都在文件mixins.js的eventspatchemixin中實現。這個mixin包含在widget類中。

基礎事件系統

這是歷史上第一個事件系統。它實現了一個簡單的總線模式。咱們有4種主要方法:

  • on : 在一個事件上註冊監聽器
  • off: 移除事件的監聽器
  • once: 註冊一個只使用一次的監聽器
  • trigger:跟蹤一個事件。這會調用全部監聽器。
    一下是一個怎麼使用事件系統的例子:
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); 

\color{#FFC125}{警告}
不鼓勵使用此事件系統,咱們計劃用擴展事件系統中的trigger-up方法替換每一個trigger方法。

擴展的事件系統

自定義事件小部件是一個更高級的系統,它模擬DOM事件API。每當一個事件被觸發時,它將「冒泡」組件樹,直到它到達根小部件,或者中止。

  • trigger_up:這是一種方法,它將建立一個小的odooEvent並將其分派到組件樹中。請注意,它將從觸發事件的組件開始
  • custom_events:這至關於事件字典,可是對於odoo事件來講。
    OdoEvent類很是簡單。它有三個公共屬性:target(觸發事件的小部件)、name(事件名稱)和data(有效負載)。它還有兩種方法:stopPropagation 和 is_stopped.。
    上一個示例能夠更新爲使用自定義事件系統:
var Widget = require('web.Widget'); var Counter = require('myModule.Counter'); var MyWidget = Widget.extend({ custom_events: { valuechange: '_onValueChange' }, start: function () { this.counter = new Counter(this); var def = this.counter.appendTo(this.$el); return $.when(def, this._super.apply(this, arguments); }, _onValueChange: function(event) { // do something with event.data.val }, }); // in Counter widget, we need to call the trigger_up method: ... this.trigger_up('valuechange', {value: someValue}); 

註冊

Odoo生態系統的一個常見需求是從外部擴展/更改基本系統的行爲(經過安裝應用程序,即不一樣的模塊)。例如,可能須要在某些視圖中添加新的小部件類型。在這種狀況下,以及其餘許多狀況下,一般的過程是建立所需的組件,而後將其添加到註冊表(註冊步驟),以使Web客戶機的其他部分知道它的存在。
一下是一些在系統中可用的註冊:

  • 字段註冊表(由「web.field_registry」導出)。字段註冊表包含Web客戶端已知的全部字段小部件。每當視圖(一般是表單或列表/看板)須要字段小部件時,這就是它將要查找的地方。典型的用例以下所示:
var fieldRegistry = require('web.field_registry'); var FieldPad = ...; fieldRegistry.add('pad', FieldPad); 

注意,每一個值都應該是AbstractField的子類

  • 視圖註冊表:此註冊表包含Web客戶端已知的全部JS視圖

(尤爲是視圖管理器)。此註冊表的每一個值都應該是AbstractView的子類

  • 動做註冊表:咱們跟蹤此註冊表中的全部客戶端動做。這個是動做管理器在須要建立客戶端操做時查找的位置。在版本11中,每一個值應該只是小部件的一個子類。可是,在版本12中,值必須是abstractAction。

小部件之間的通訊

有許多組件之間的通訊方式

  • 從父級到它的子級
    一個簡單的例子。父不見能夠簡單的調用子部件方法:
    this.someWidget.update(someInfo);

  • 從一個小部件到它的父/某個祖先
    在這種狀況下,小部件的工做只是通知其環境發生了什麼事情。因爲咱們不但願小部件具備對其父部件的引用(這將使小部件與其父部件的實現相結合),所以繼續操做的最佳方法一般是使用觸發器trigger_up方法觸發一個事件,該事件將冒泡到組件樹中:
    this.trigger_up('open_record', { record: record, id: id});
    此事件將在小部件上觸發,而後將冒泡並最終被某些上游小部件捕獲:

var SomeAncestor = Widget.extend({ custom_events: { 'open_record': '_onOpenRecord', }, _onOpenRecord: function (event) { var record = event.data.record; var id = event.data.id; // do something with the event. }, }); 
  • 交叉組件
    經過總線能夠實現跨組件通訊。這不是首選的通訊形式,由於它有使代碼難以維護的缺點。可是,它具備分離組件的優點。在這種狀況下,這只是經過觸發和監聽總線上的事件來完成的。例如:
// in WidgetA var core = require('web.core'); var WidgetA = Widget.extend({ ... start: function () { core.bus.on('barcode_scanned', this, this._onBarcodeScanned); }, }); // in WidgetB var WidgetB = Widget.extend({ ... someFunction: function (barcode) { core.bus.trigger('barcode_scanned', barcode); }, }); 

在本例中,咱們使用web.core導出的總線,但這不是必需的。能夠爲特定目的建立總線。

服務services

在11.0版中,咱們引入了服務的概念。主要的想法是給子組件一種受控制的方式來訪問它們的環境,這種方式容許框架進行足夠的控制,而且是可測試的。
服務系統圍繞三個理念進行組織:services、service providers和widget。它的工做方式是小部件觸發(使用trigger_up)事件,這些事件冒泡到服務提供者,服務提供者將要求服務執行任務,而後可能返回一個答案。

服務service

服務是AbstractService類的實例。它基本上只有一個名稱和一些方法。它的工做是執行一些工做,一般是一些依賴於環境的工做。
例如,咱們有Ajax服務(任務是執行RPC)、本地存儲(與瀏覽器本地存儲交互)和許多其餘服務。
如下是有關如何實現Ajax服務的簡化示例:

var AbstractService = require('web.AbstractService'); var AjaxService = AbstractService.extend({ name: 'ajax', rpc: function (...) { return ...; }, }); 

這個服務被叫作‘ajax’並且定義了一個方法,rpc.

服務提供者Service Provider

爲了使服務正常工做,有必要讓一個服務提供者準備好分派定製事件。在後端(Web客戶端),這是由主Web客戶端實例完成的。請注意,服務提供程序的代碼來自ServiceProviderMin。

部件widget

小部件是請求服務的部分。爲了作到這一點,它只需觸發一個事件調用服務(一般經過使用helper函數調用)。此事件將冒泡並將意圖傳達給系統的其他部分。
在實踐中,有些函數被頻繁地調用,以致於咱們有一些助手函數使它們更容易使用。例如,_rpc方法是幫助生成rpc的助手。

var SomeWidget = Widget.extend({ _getActivityModelViewID: function (model) { return this._rpc({ model: model, method: 'get_activity_view_id' }); }, }); 

\color{#FFC125}{警告}
若是一個小部件被銷燬,它將從主組件樹中分離出來,而且沒有父組件。在這種狀況下,事件不會冒泡,這意味着工做不會完成。這一般正是咱們從一個被破壞的小部件中想要的。

RPCs

RPC功能由Ajax服務提供。但大多數人可能只會與_rpc助手進行交互。
在處理odoo時,一般有兩個用例:一個須要在(python)模型上調用方法(這須要經過控制器call_kw),或者一個須要直接調用控制器(在某些路由上可用)。

  • 在python模型中調用方法
return this._rpc({ model: 'some.model', method: 'some_method', args: [some, args], }); 
  • 直接調用控制器
return this._rpc({ route: '/some/route/', params: { some: kwargs}, }); 

通知

odoo框架有一種標準的方式來向用戶傳遞各類信息:通知,它顯示在用戶界面的右上角。
通知有兩種類型:

  • notification: 有助於顯示一些反饋。例如,每當用戶取消訂閱某個頻道時。
  • warning:用於顯示一些重要/緊急信息。一般是系統中的大多數(可恢復的)錯誤。

此外,通知還能夠用於向用戶詢問問題,而不會干擾其工做流。想象一下,經過VoIP接收到的一個電話呼叫:能夠顯示一個帶有兩個按鈕的粘性通知:接受和拒絕。

通知系統

Odoo中的通知系統設計有如下組件:

  • a Notification widget:這是一個簡單的小部件,用於建立和顯示所需的信息。
  • a NotificationService:一種服務,其職責是在請求完成時(使用custom_event)建立和銷燬通知。請注意,Web客戶端是一個服務提供者。
  • ServiceMixin中的兩個助手功能:do_notify和do_warn

顯示通知

顯示通知的最多見方法是使用來自ServiceMixin的兩種方法:

  • do_notify(title, message, sticky, className):
    顯示一個通知類型的通知:
       * title:string. 將會在頂部顯示爲標題
       * message:string. 通知的內容
       * sticky : boolean,optional. 若是爲真,這個通知將會一直保留直到用戶解除。不然,通知將會在一段很短的時間以後自動關閉。
       * calssname:string,optional.這是一個css類的名字,將會自動添加到通知中。這對樣式有用,即便不鼓勵使用它。

  • do_warn(title, message, sticky, className):
    顯示一個警告類型的通知。
       * title:string.將會在頂部顯示爲標題
       * message:string.通知的內容
       * sticky : boolean,optional. 若是爲真,這個通知將會一直保留直到用戶解除。不然,通知將會在一段很短的時間以後自動關閉。
       * calssname:string,optional.這是一個css類的名字,將會自動添加到通知中。這對樣式有用,即便不鼓勵使用它。
    這裏有兩個如何使用這兩個方法的例子:

// note that we call _t on the text to make sure it is properly translated. this.do_notify(_t("Success"), _t("Your signature request has been sent.")); this.do_warn(_t("Error"), _t("Filter name is required.")); 

任務欄Systray

Systray是界面菜單欄的右側部分,Web客戶端在其中顯示一些小部件,如消息菜單。
當菜單建立SystrayMenu時,它將查找全部已註冊的小部件,並將它們做爲子小部件添加到適當的位置。
目前沒有針對Systray小工具的特定API。它們應該是簡單的小部件,而且能夠像使用trigger-up方法的其餘小部件同樣與環境通訊。

添加一個新得任務欄項目

沒有Systray註冊表。添加小部件的正確方法是將其添加到類變量systraymenu.items中。

var SystrayMenu = require('web.SystrayMenu'); var MySystrayWidget = Widget.extend({ ... }); SystrayMenu.Items.push(MySystrayWidget); 

排序Ordering

在將小部件添加到本身以前,Systray菜單將按Sequence屬性對項目進行排序。若是原型上不存在該屬性,則將使用50。所以,要將Systray項目定位在靠後,能夠設置一個很是高的序列號(反之,將其放在靠前的是一個較低的序列號)。
MySystrayWidget.prototype.sequence = 100;

翻譯管理

有些翻譯是在服務器端進行的(基本上是由服務器呈現或處理的全部文本字符串),可是靜態文件中有須要翻譯的字符串。它目前的工做方式以下:

  • 每一個可翻譯字符串都帶有特殊的函數_t(可在js模塊web.core中找到)
  • 服務器使用這些字符串生成正確的PO文件。
  • 每當加載Web客戶機時,它將調用route/web/web client/translations,它返回全部可翻譯術語的列表
  • 在運行時,每當調用函數時,它都會在該列表中查找以查找轉換,若是找不到轉換,則返回它或原始字符串。

請注意,在文檔翻譯模塊中,從服務器的角度對翻譯進行了更詳細的解釋。
javascript中的翻譯有兩個重要功能:_t和_lt。區別在於_lt是以懶惰的方式進行的。

var core = require('web.core'); var _t = core._t; var _lt = core._lt; var SomeWidget = Widget.extend({ exampleString: _lt('this should be translated'), ... someMethod: function () { var str = _t('some text'); ... }, }); 

在本例中,因爲在加載模塊時翻譯還沒有準備就緒,所以必須使用_lt。
注意,翻譯功能須要注意。參數中給定的字符串不該是動態的。

會話Session

Web客戶端提供了一個特定的模塊,其中包含一些特定於用戶當前會話的信息。一些顯著的鍵是:

  • uid:當前用戶的id(來自於表 res.users 的ID)
  • user_name: 用戶的名字,字符串類型
  • 用戶上下文(context [用戶id,語言和時區])
  • partner_id: 與當前用戶關聯的合做夥伴的ID
  • db:當前使用的數據庫名字

添加信息到會話中

加載/web路由後,服務器將在模板中插入一些會話信息和腳本標記。信息將從模型ir.http的方法session_info中讀取。所以,若是要添加特定信息,能夠經過重寫session_info方法並將其添加到字典中來完成。

from odoo import models from odoo.http import request class IrHttp(models.AbstractModel): _inherit = 'ir.http' def session_info(self): result = super(IrHttp, self).session_info() result['some_key'] = get_some_value_from_db() return result 

如今,經過在會話中讀取該值,能夠在javascript中獲取該值:

var session = require('web.session'); var myValue = session.some_key; ... 

請注意,此機制旨在減小Web客戶端準備就緒所需的通訊量。它更適合於計算成本較低的數據(緩慢的session_info調用將延遲爲每一個人加載Web客戶端),以及在初始化過程早期須要的數據。

視圖

「視圖」一詞有多種含義。本節是關於視圖的JavaScript代碼的設計,而不是Arch的結構或其餘任何內容。
2017年,Odoo用新架構替換了先前的視圖代碼。主要須要將呈現邏輯與模型邏輯分開。
視圖(通常意義上)如今用4個部分描述:視圖、控制器、渲染器和模型。這4個部分的API在AbstractView、AbstractController、AbstractRenderer和AbstractModel類中進行了描述。


 
視圖結構.png
  • 視圖是工廠。它的工做是獲取一組字段、arch、上下文和其餘一些參數,而後構造一個控制器/渲染器/模型三元組。
    視圖的做用是用正確的信息正確地設置MVC模式的每一部分。一般,它必須處理arch字符串,並提取視圖中彼此所需的數據。
    請注意,視圖是一個類,而不是一個小部件。一旦它的工做完成,它就能夠被丟棄。
  • 渲染器有一個做業:表示在DOM元素中查看的數據。每一個視圖均可以以不一樣的方式呈現數據。此外,它應該監聽適當的用戶操做,並在必要時通知其父級(Controller)。
    渲染器是MVC模式中的V.
  • 模型:它的工做是獲取並保持視圖的狀態。一般,它以某種方式表示數據庫中的一組記錄。該模型是「業務數據」的全部者。它是MVC模式中的M.
  • Controller:它的工做是協調渲染器和模型。此外,它是Web客戶端其他部分的主要入口點。例如,當用戶在搜索視圖中更改某些內容時,將使用適當的信息調用控制器的更新方法。
    它是MVC模式中的C.

視圖的JS代碼已設計爲可在視圖管理器/操做管理器的上下文以外使用。它們能夠用於客戶端操做,也能夠顯示在公共網站上(對資源進行一些處理)

字段部件

該AbstractField類是在一個視圖中的全部控件的基類,用於支持他們全部的視圖(目前爲:表格,列表,看板)。
v11字段小部件與先前版本之間存在許多差別。讓咱們提一下最重要的一些:

  • 小部件在全部視圖之間共享(表單/列表/看板)。無需再複製實現。請注意,能夠爲視圖設置特定版本的窗口小部件,方法是在視圖註冊表中爲視圖名稱添加前綴:list.many2one將優先於many2one選擇。
  • 小部件再也不是字段值的全部者。它們僅表示數據並與視圖的其他部分進行通訊。
  • 小部件再也不須要可以在編輯和只讀模式之間切換。如今,當須要進行此類更改時,窗口小部件將被銷燬並再次從新呈現。這不是問題,由於他們不管如何都不擁有本身的價值
  • 字段小部件能夠在視圖以外使用。他們的API略顯笨拙,但它們的設計是獨立的。

裝飾與列表視圖同樣,字段小部件對裝飾具備簡單的支持。裝飾的目標是有一種簡單的方法來根據記錄當前狀態指定文本顏色。例如,

<field name = 「state」 decoration-danger = 「amount&lt; 10000」 /> 

有效的裝飾名字有:

  • decoration-bf
  • decoration-it
  • decoration-danger
  • decoration-info
  • decoration-muted
  • decoration-primary
  • decoration-success
  • decoration-warning 每一個裝飾decoration-X將映射到css類text-X,這是一個標準的bootstrap css類(text-it和text-bf除外,它們由odoo處理並分別對應於斜體和粗體)。請注意,decoration屬性的值應該是一個有效的python表達式,它將使用記錄做爲評估上下文進行評估。
做者:dooms21day 連接:https://www.jianshu.com/p/1a47fac01077 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索