Javascript速查表?
在JavaScript和Odoo中,解決問題有很多方法。然而,Odoo框架被設計成可擴展的(這是一個相當大的限制),一些常見問題有一個不錯的標準解決方案。標準解決方案可能有易于理解的優點,對于Odoo開發人員來說,當Odoo被修改時,它可能仍然有效。
本文檔試圖解釋如何解決其中一些問題。請注意,這不是一個參考文獻。這只是一些隨機的 配方或解釋,在某些情況下如何進行。
首先,記住使用JS自定義Odoo的第一條規則是: 嘗試在Python中完成 。這可能看起來很奇怪,但Python框架非??蓴U展,許多行為可以通過一點點的xml或Python輕松完成。這通常比使用JS的維護成本更低:
JS 框架往往更容易變化,因此 JS 代碼需要更頻繁地更新
如果需要與服務器通信并與JavaScript框架進行良好的集成,實現自定義行為通常更加困難??蚣芴幚砹嗽S多小細節,需要自定義代碼進行復制。例如,響應性、更新URL或無閃爍地顯示數據。
注解
本文檔并不真正解釋任何概念。這更像是一本食譜。有關更多詳細信息,請參閱javascript參考頁面(請參閱 Javascript 參考手冊 )
創建一個新的字段小部件?
這可能是一個非常常見的用例:我們想以非常特定的方式(可能是業務相關的)在表單視圖中顯示一些信息。例如,假設我們想根據某些業務條件更改文本顏色。
這可以通過三個步驟完成:創建一個新的小部件,將其注冊到字段注冊表中,然后將小部件添加到表單視圖中的字段中。
- 創建一個新的小部件:
這可以通過擴展小部件來完成:
var FieldChar = require('web.basic_fields').FieldChar; var CustomFieldChar = FieldChar.extend({ _renderReadonly: function () { // implement some custom logic here }, });
- 在字段注冊表中注冊它:
Web客戶端需要知道小部件名稱與其實際類之間的映射關系。這是通過注冊表完成的:
var fieldRegistry = require('web.field_registry'); fieldRegistry.add('my-custom-field', CustomFieldChar);
- 在表單視圖中添加小部件
<field name="somefield" widget="my-custom-field"/>
請注意,只有表單、列表和看板視圖使用此字段小部件注冊表。這些視圖緊密集成,因為列表和看板視圖可以出現在表單視圖內。
修改現有字段小部件?
另一個用例是我們想要修改現有的字段小部件。例如,Odoo中的VoIP插件需要修改FieldPhone小部件以添加在VoIP上輕松撥打給定號碼的可能性。這是通過 包含 FieldPhone小部件來完成的,因此無需更改任何現有的表單視圖。
Field Widgets(AbstractField 的實例(子類))與其他小部件一樣,因此它們可以進行 monkey patch。如下所示:
var basic_fields = require('web.basic_fields');
var Phone = basic_fields.FieldPhone;
Phone.include({
events: _.extend({}, Phone.prototype.events, {
'click': '_onClick',
}),
_onClick: function (e) {
if (this.mode === 'readonly') {
e.preventDefault();
var phoneNumber = this.value;
// call the number on voip...
}
},
});
請注意,無需將小部件添加到注冊表中,因為它已經注冊。
從界面修改主要小部件?
另一個常見的用例是需要自定義用戶界面中的某些元素。例如,在主菜單中添加消息。在這種情況下,通常的過程是再次 包含 小部件。這是唯一的方法,因為這些小部件沒有注冊表。
通常使用以下代碼來完成此操作:
var HomeMenu = require('web_enterprise.HomeMenu');
HomeMenu.include({
render: function () {
this._super();
// do something else here...
},
});
創建新視圖(從頭開始)?
創建新視圖是一個更高級的主題。這個速查表只會強調可能需要完成的步驟(沒有特定的順序):
將新的視圖類型添加到
ir.ui.view
的type
字段中:class View(models.Model): _inherit = 'ir.ui.view' type = fields.Selection(selection_add=[('map', "Map")])
將新視圖類型添加到
ir.actions.act_window.view
的view_mode
字段中:class ActWindowView(models.Model): _inherit = 'ir.actions.act_window.view' view_mode = fields.Selection(selection_add=[('map', "Map")])
- 創建四個主要部分以構建視圖(使用JavaScript):
我們需要一個視圖(
AbstractView
的子類,這是工廠),一個渲染器(來自AbstractRenderer
),一個控制器(來自AbstractController
)和一個模型(來自AbstractModel
)。我建議從簡單地擴展超類開始:var AbstractController = require('web.AbstractController'); var AbstractModel = require('web.AbstractModel'); var AbstractRenderer = require('web.AbstractRenderer'); var AbstractView = require('web.AbstractView'); var MapController = AbstractController.extend({}); var MapRenderer = AbstractRenderer.extend({}); var MapModel = AbstractModel.extend({}); var MapView = AbstractView.extend({ config: { Model: MapModel, Controller: MapController, Renderer: MapRenderer, }, });
- 將視圖添加到注冊表:
通常情況下,視圖類型與實際類之間的映射需要更新:
var viewRegistry = require('web.view_registry'); viewRegistry.add('map', MapView);
- 實現四個主要類:
The
View
類需要解析arch
字段并設置其他三個類。Renderer
負責在用戶界面中表示數據,Model
應該與服務器通信,加載數據并處理它。Controller
用于協調,與Web客戶端交流,…
- 在數據庫中創建一些視圖:
<record id="customer_map_view" model="ir.ui.view"> <field name="name">customer.map.view</field> <field name="model">res.partner</field> <field name="arch" type="xml"> <map latitude="partner_latitude" longitude="partner_longitude"> <field name="name"/> </map> </field> </record>
自定義現有視圖?
假設我們需要創建一個通用視圖的自定義版本。例如,一個帶有額外的“絲帶狀”小部件的看板視圖(用于顯示一些特定的自定義信息)。在這種情況下,可以分為三個步驟:擴展看板視圖(這也可能意味著擴展控制器/渲染器和/或模型),然后在視圖注冊表中注冊視圖,最后在看板架構中使用視圖(一個具體的例子是幫助臺儀表板)。
- 擴展視圖:
這是它可能看起來的樣子:
var HelpdeskDashboardRenderer = KanbanRenderer.extend({ ... }); var HelpdeskDashboardModel = KanbanModel.extend({ ... }); var HelpdeskDashboardController = KanbanController.extend({ ... }); var HelpdeskDashboardView = KanbanView.extend({ config: _.extend({}, KanbanView.prototype.config, { Model: HelpdeskDashboardModel, Renderer: HelpdeskDashboardRenderer, Controller: HelpdeskDashboardController, }), });
- 將其添加到視圖注冊表:
通常情況下,我們需要告知 Web 客戶端視圖名稱與實際類之間的映射關系。
var viewRegistry = require('web.view_registry'); viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
- 在實際視圖中使用它:
我們現在需要通知Web客戶端,一個特定的
ir.ui.view
需要使用我們的新類。請注意,這是一個Web客戶端特定的問題。從服務器的角度來看,我們仍然有一個看板視圖。正確的方法是在arch的根節點上使用一個特殊的屬性js_class
(這個屬性將在將來重命名為widget
,因為這真的不是一個好名字):<record id="helpdesk_team_view_kanban" model="ir.ui.view" > ... <field name="arch" type="xml"> <kanban js_class="helpdesk_dashboard"> ... </kanban> </field> </record>
注解
注意:您可以更改視圖解釋 arch 結構的方式。但是,從服務器的角度來看,這仍然是相同基礎類型的視圖,受到相同的規則(例如 rng 驗證)的約束。因此,您的視圖仍需要具有有效的 arch 字段。
Promises 和異步代碼?
非常好的、全面的 Promise 入門教程,請閱讀這篇優秀的文章 https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md
創建新的 Promise?
- 將常量轉換為 Promise
Promise 有兩個靜態函數,可以基于常量創建一個已解決或已拒絕的 Promise:
var p = Promise.resolve({blabla: '1'}); // creates a resolved promise p.then(function (result) { console.log(result); // --> {blabla: '1'}; }); var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise p2.catch(function (reason) { console.log(reason); // --> {error: 'error message'); });
注解
請注意,即使 Promise 已經被創建并且已經被解決或拒絕,
then
或catch
處理程序仍然會異步調用。
- 基于已經異步化的代碼
假設在一個函數中,您必須執行一個 rpc,當它完成后將結果設置在
this
上。this._rpc
是一個返回Promise
的函數。function callRpc() { var self = this; return this._rpc(...).then(function (result) { self.myValueFromRpc = result; }); }
- 用于基于回調的函數
假設你正在使用一個函數
this.close
,它以回調函數作為參數,當關閉完成時調用該回調函數?,F在假設你正在一個必須發送一個在關閉完成時解決的 promise 的方法中執行此操作。1 function waitForClose() { 2 var self = this; 3 return new Promise (function(resolve, reject) { 4 self.close(resolve); 5 }); 6 }
第二行:我們將
this
保存到一個變量中,這樣在內部函數中,我們就可以訪問組件的作用域- 第3行:我們創建并返回一個新的 Promise。Promise 的構造函數接受一個函數作為參數。這個函數本身有兩個參數,我們在這里稱之為
resolve
和reject
。 resolve
是一個函數,調用它會將 Promise 置于已解決狀態。reject
是一個函數,調用它會將 Promise 置于被拒絕的狀態。我們這里不使用 reject,可以省略。
- 第3行:我們創建并返回一個新的 Promise。Promise 的構造函數接受一個函數作為參數。這個函數本身有兩個參數,我們在這里稱之為
第4行:我們在對象上調用close函數。它以函數作為參數(回調函數),恰好resolve已經是一個函數,因此我們可以直接傳遞它。為了更清晰,我們可以這樣寫:
return new Promise (function (resolve) { self.close(function () { resolve(); }); });
- 創建一個 Promise 生成器(按順序調用一個 Promise,等待最后一個 Promise)
假設您需要循環遍歷一個數組,在 順序 中執行一個操作,并在最后一個操作完成時解析一個承諾。
function doStuffOnArray(arr) { var done = Promise.resolve(); arr.forEach(function (item) { done = done.then(function () { return item.doSomethingAsynchronous(); }); }); return done; }
這樣,你返回的 Promise 實際上是最后一個 Promise。
- 創建一個 Promise,然后在其定義范圍之外解決它(反模式)
注解
我們不建議使用這個,但有時它是有用的。首先仔細考慮替代方案…
... var resolver, rejecter; var prom = new Promise(function (resolve, reject){ resolver = resolve; rejecter = reject; }); ... resolver("done"); // will resolve the promise prom with the result "done" rejecter("error"); // will reject the promise prom with the reason "error"
等待 Promise?
- 等待一定數量的 Promises
如果您有多個需要等待的承諾,您可以將它們轉換為單個承諾,當所有承諾都得到解決時,該承諾將得到解決,使用 Promise.all(arrayOfPromises)。
var prom1 = doSomethingThatReturnsAPromise(); var prom2 = Promise.resolve(true); var constant = true; var all = Promise.all([prom1, prom2, constant]); // all is a promise // results is an array, the individual results correspond to the index of their // promise as called in Promise.all() all.then(function (results) { var prom1Result = results[0]; var prom2Result = results[1]; var constantResult = results[2]; }); return all;
- 等待承諾鏈的一部分,但不等待另一部分
如果您有一個異步進程,想要等待它完成某些操作,但同時也想在此操作完成之前返回給調用者。
function returnAsSoonAsAsyncProcessIsDone() { var prom = AsyncProcess(); prom.then(function (resultOfAsyncProcess) { return doSomething(); }); /* returns prom which will only wait for AsyncProcess(), and when it will be resolved, the result will be the one of AsyncProcess */ return prom; }
錯誤處理?
- 通常在 Promises 中
一般的想法是,承諾不應該因為控制流而被拒絕,而只應該因為錯誤而被拒絕。當這種情況發生時,您將有多個承諾的解決方案,例如狀態代碼,您需要在
then
處理程序中進行檢查,并在承諾鏈的末尾有一個單獨的catch
處理程序。function a() { x.y(); // <-- this is an error: x is undefined return Promise.resolve(1); } function b() { return Promise.reject(2); } a().catch(console.log); // will log the error in a a().then(b).catch(console.log); // will log the error in a, the then is not executed b().catch(console.log); // will log the rejected reason of b (2) Promise.resolve(1) .then(b) // the then is executed, it executes b .then(...) // this then is not executed .catch(console.log); // will log the rejected reason of b (2)
- 特別是在Odoo中
在Odoo中,我們經常使用promise rejection來控制流程,例如在
web.concurrency
模塊中定義的互斥鎖和其他并發原語中。我們還希望出于 業務 原因執行catch,但不希望在promise或處理程序的定義中存在編碼錯誤時執行catch。為此,我們引入了guardedCatch
的概念。它類似于catch
,但當拒絕的原因不是錯誤時不會被調用。function blabla() { if (someCondition) { return Promise.reject("someCondition is truthy"); } return Promise.resolve(); } // ... var promise = blabla(); promise.then(function (result) { console.log("everything went fine"); }) // this will be called if blabla returns a rejected promise, but not if it has an error promise.guardedCatch(function (reason) { console.log(reason); }); // ... var anotherPromise = blabla().then(function () { console.log("everything went fine"); }) // this will be called if blabla returns a rejected promise, // but not if it has an error .guardedCatch(console.log);
var promiseWithError = Promise.resolve().then(function () { x.y(); // <-- this is an error: x is undefined }); promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called promiseWithError.catch(function (reason) {console.log(reason);}); // will be called
測試異步代碼?
- 在測試中使用 Promises
在測試代碼中,我們支持最新版本的JavaScript,包括
async
和await
等基本功能。這使得使用和等待 Promise 變得非常容易。大多數輔助方法也會返回一個 Promise(通過標記為async
或直接返回一個 Promise)。var testUtils = require('web.test_utils'); QUnit.test("My test", async function (assert) { // making the function async has 2 advantages: // 1) it always returns a promise so you don't need to define `var done = assert.async()` // 2) it allows you to use the `await` assert.expect(1); var form = await testUtils.createView({ ... }); await testUtils.form.clickEdit(form); await testUtils.form.click('jquery selector'); assert.containsOnce('jquery selector'); form.destroy(); }); QUnit.test("My test - no async - no done", function (assert) { // this function is not async, but it returns a promise. // QUnit will wait for for this promise to be resolved. assert.expect(1); return testUtils.createView({ ... }).then(function (form) { return testUtils.form.clickEdit(form).then(function () { return testUtils.form.click('jquery selector').then(function () { assert.containsOnce('jquery selector'); form.destroy(); }); }); }); }); QUnit.test("My test - no async", function (assert) { // this function is not async and does not return a promise. // we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback assert.expect(1); var done = assert.async(); testUtils.createView({ ... }).then(function (form) { testUtils.form.clickEdit(form).then(function () { testUtils.form.click('jquery selector').then(function () { assert.containsOnce('jquery selector'); form.destroy(); done(); }); }); }); });
如您所見,更好的形式是使用
async/await
,因為它更清晰、更簡潔。