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.viewtype 字段中:

    class View(models.Model):
        _inherit = 'ir.ui.view'
    
        type = fields.Selection(selection_add=[('map', "Map")])
    
  • 將新視圖類型添加到 ir.actions.act_window.viewview_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 已經被創建并且已經被解決或拒絕, thencatch 處理程序仍然會異步調用。

  • 基于已經異步化的代碼

    假設在一個函數中,您必須執行一個 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 的構造函數接受一個函數作為參數。這個函數本身有兩個參數,我們在這里稱之為 resolvereject 。
      • resolve 是一個函數,調用它會將 Promise 置于已解決狀態。

      • reject 是一個函數,調用它會將 Promise 置于被拒絕的狀態。我們這里不使用 reject,可以省略。

    • 第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,包括 asyncawait 等基本功能。這使得使用和等待 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 ,因為它更清晰、更簡潔。