Готовые рецепты для JavaScript

Есть много способов решить проблему как в JavaScript, так и в Odoo. Тем не менее, фреймворк Odoo был разработан для того чтобы быть расширяемым (это довольно большое ограничение), и некоторые распространенные проблемы имеют хорошее стандартное решение. Стандартное решение имеет преимущество в том, что его легко понять разработчикам odoo, и, скорее всего, оно будет работать, когда Odoo изменится в процессе развития.

Этот документ пытается объяснить, как можно решить некоторые из этих проблем. Обратите внимание, что это не справка. Это просто случайная коллекция рецептов или объяснения того, как действовать в некоторых случаях.

Прежде всего, помните, что первое правило настройки odoo с помощью JS: попробуйте сделать это в python. Это может показаться странным, но среда Python достаточно расширяема, и многие варианты поведения могут быть реализованы просто с помощью xml или python. Как правило, такое решение имеет более низкую стоимость обслуживания, чем работа с JS:

  • сам JS фреймворк имеет тенденцию меняться больше,что приводит к необходимости чаще менять JS код
  • часто бывает сложнее реализовать настраиваемое поведение, если ему необходимо взаимодействовать с сервером и должным образом интегрироваться во фреймворк javascript. Фреймворк требует много мелких деталей, которые нужно реплицировать в коде. Например, отзывчивость, или обновление URL, или отображение данных без мерцания.

Создание нового поля виджета

Скорее всего это очень распространенный сценарий использования: мы хотим отображать информацию в представлении Form действительно особенным(возможно, бизнес-зависимым) способом. Например, мы хотим изменить цвет текста в зависимости от бизнес-условия.

Это можно сделать в три этапа: создать новый виджет, зарегистрировать его в реестре полей, затем добавить виджет в поле в представления Form

  • создание нового виджета:

    Это можно сделать, расширив виджет:

    var FieldChar = require('web.basic_fields').FieldChar;
    
    var CustomFieldChar = FieldChar.extend({
        _renderReadonly: function () {
            // implement some custom logic here
        },
    });
    
  • регистрация в реестре полей:

    Веб-клиент должен знать как сопоставить имя виджета и его класс. Это делается реестром:

    var fieldRegistry = require('web.field_registry');
    
    fieldRegistry.add('my-custom-field', CustomFieldChar);
    
  • добавляем виджет в представление Form
    <field name="somefield" widget="my-custom-field"/>
    

    Обратите внимание, что только поля представлений Form, List и Kanban используют это поле реестра виджетов. Эти представления тесно интегрированы, поскольку представления List и Kanban могут отображаться внутри представления Form).

Изменение существующего виджета поля

Другой вариант использования - мы хотим изменить существующий виджет поля. Например, аддон voip в odoo должен изменить виджет FieldPhone, чтобы добавить возможность легко позвонить по указанному номеру в voip. Это делается путем including виджета FieldPhone, поэтому нет необходимости изменять какое-либо существующее представление Form.

Виджеты полей (экземпляры (подкласса) AbstractField), как и все остальные виджеты, могут быть «пропатчены по обезьяньи». Это выглядит так:

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...
        }
    },
});

Обратите внимание, что нет необходимости добавлять виджет в реестр, так как он уже зарегистрирован.

Модификация основного виджета из интерфейса

Другим распространенным вариантом использования является необходимость настройки некоторых элементов из пользовательского интерфейса. Например, добавить сообщение в домашнее меню. Обычный процесс в этом случае снова включает include виджета. Это единственный способ сделать это, поскольку для этих виджетов нет реестров.

Обычно это делается с помощью подобного кода:

var HomeMenu = require('web_enterprise.HomeMenu');

HomeMenu.include({
    render: function () {
        this._super();
        // do something else here...
    },
});

Создание нового представления (с нуля)

Создание нового представления - более сложная тема. Этот рецепт содержит только те шаги, которые нужно будет выполнить (порядок выполнения не обязательно такой же):

  • добавление нового типа представления в поле type в модель``ir.ui.view``:

    class View(models.Model):
        _inherit = 'ir.ui.view'
    
        type = fields.Selection(selection_add=[('map', "Map")])
    
  • добавление нового типа представления в поле view_mode модели ir.actions.act_window.view:

    class ActWindowView(models.Model):
        _inherit = 'ir.actions.act_window.view'
    
        view_mode = fields.Selection(selection_add=[('map', "Map")])
    
  • Создание четырех основных частей, из которых создается представление (в JavaScript):

    нам нужно представление (субкласс AbstractView, это фабрика), рендерер (от AbstractRenderer), контроллер (от AbstractController) и модель (from 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);
    
  • реализация четырех основных классов:
    Классу View необходимо проанализировать поле arch и настроить остальные три класса. Renderer отвечает за представление данных в пользовательском интерфейсе, Model должен общаться с сервером, загружать данные и обрабатывать их. И Controller предназначен для координации, общения с веб-клиентом, …
  • создание представлений в базе данных:
    <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>
    

Кастомизация существующего представления

Предположим, нам нужно создать пользовательскую версию универсального представления. Например, представление Kanban с дополнительным * ribbon-like* виджетом сверху (для отображения пользовательской информации). В этом случае это можно сделать за 3 шага: расширить представление Kanban (что также, вероятно, означает расширение контроллеров/рендеров и/или моделей), затем зарегистрировать представление в реестре представлений и, наконец, использовать представление в атрибуте arch. (конкретный пример - панель инструментов службы поддержки).

  • расширение представления:

    Вот как это может выглядеть:

    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,
        }),
    });
    
  • добавление в реестр представлений:

    как обычно, мы должны информировать веб-клиента о сопоставлении имени представления и его класса.

    var viewRegistry = require('web.view_registry');
    viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
    
  • используем в действующем представлении:

    Теперь нам нужно сообщить веб-клиенту, что конкретному ir.ui.view необходимо использовать наш новый класс. Обратите внимание, что это особая потребность веб-клиента. С точки зрения сервера у нас все еще есть представление Kanban. Правильный способ сделать это - использовать специальный атрибут js_class (который когда-нибудь будет переименован в widget, потому что это действительно не очень хорошее имя) в корневом узле arch:

    <record id="helpdesk_team_view_kanban" model="ir.ui.view" >
        ...
        <field name="arch" type="xml">
            <kanban js_class="helpdesk_dashboard">
                ...
            </kanban>
        </field>
    </record>
    

Promise’ы и асинхронный код

Для очень хорошего и полного введения в Promise, пожалуйста, прочитайте эту превосходную статью https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md

Создание новых 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');
    });
    
  • на основе уже асинхронного кода

    Предположим, что в функции вы должны выполнить rpc запрос, и когда он будет завершен, установите результат на это событие. this._rpc - это функция, которая возвращает Promise.

    function callRpc() {
        var self = this;
        return this._rpc(...).then(function (result) {
            self.myValueFromRpc = result;
        });
    }
    
  • для функции обратного вызова

    Предположим, что вы использовали функцию this.close, которая принимает в качестве параметра обратный вызов(колбэк), который вызывается по окончании закрытия. Теперь предположим, что вы делаете это в методе, который должен отправить Promise, которое разрешается после завершения закрытия.

    1
    2
    3
    4
    5
    6
    function waitForClose() {
        var self = this;
        return new Promise (function(resolve, reject) {
            self.close(resolve);
        });
    }
    
    • строка 2: мы сохраняем this в переменную, чтобы во внутренней функции мы могли получить доступ к области действия нашего компонента
    • строка 3: мы создаем и возвращаем новый Promise. Конструктор Promise принимает функцию в качестве параметра. Эта функция сама имеет 2 параметра, которые мы назвали здесь resolve и reject
      • resolve это функция, которая при вызове переводит 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

  • ожидание некоторого количества Promise

    если у вас есть несколько Promise, которые нужно ждать, вы можете преобразовать их в один Promise, которое будет разрешено, когда все Promise будут разрешены с помощью 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;
    
  • ожидая часть цепочки Promise, но не другую часть

    Если у вас есть асинхронный процесс, который вы хотите подождать, чтобы что-то сделать, но вы также хотите вернуться к вызывающей стороне, прежде чем что-то сделать.

    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;
    }
    

Обработка ошибок

  • в целом в Promise

    Общая идея заключается в том, что Promise не должен быт отклонен для потока управления, а должен быть отклонен только для ошибок. В этом случае у вас будет несколько разрешений вашего Promise, например, с кодами состояния, которые вы должны будете проверить в обработчиках then и одиночном обработчике catch в конце цепочки Promise.

    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 для потока управления, например в мьютексах и других примитивах параллелизма, определенных в модуле web.concurrency. Мы также хотим выполнить перехват по бизнес причинам , но не в случае ошибки при написании кода в определении Promise или обработчиков. Для этого мы ввели понятие 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
    

Тестирование асинхронного кода

  • использование Promise в тестах

    В коде тестов мы поддерживаем последнюю версию 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, так как это проще и короче писать.