Довідник Javascript

Існує багато способів вирішення проблеми в JavaScript і Odoo. Однак фреймворк Odoo був розроблений таким чином, щоб бути розширюваним (це досить велике обмеження), і деякі типові проблеми мають гарне стандартне рішення. Стандартне рішення, ймовірно, має перевагу в тому, що його легко зрозуміти розробнику odoo, і, ймовірно, продовжуватиме працювати, коли Odoo буде змінено.

Цей документ намагається пояснити, як можна вирішити деякі з цих проблем. Зауважте, що це не посилання. Це лише випадкова колекція рецептів або пояснень, як діяти в деяких випадках.

Перш за все, пам’ятайте, що перше правило налаштування odoo за допомогою JS таке: спробуйте зробити це на python. Це може здатися дивним, але фреймворк Python досить розширюваний, і багато функцій можна виконати просто за допомогою xml або python. Це зазвичай має нижчу вартість обслуговування, ніж робота з JS:

  • фреймворк JS має тенденцію змінюватися більше, тому код JS потрібно оновлювати частіше

  • часто важче реалізувати налаштовану поведінку, якщо вона потребує зв’язку з сервером і належним чином інтегрується з фреймворком JavaScript. Фреймворк має багато дрібних деталей, які потрібно відтворити налаштованому коду. Наприклад, швидкість реагування, або оновлення URL-адреси, або відображення даних без мерехтіння.

Примітка

Цей документ насправді не пояснює жодних понять. Це більше кулінарна книга. Щоб отримати докладнішу інформацію, зверніться до довідкової сторінки javascript (див. Посилання Javascript)

Створення нового віджета поля

Ймовірно, це дуже поширений випадок використання: ми хочемо відобразити деяку інформацію у представленні форми в дійсно специфічний (можливо, залежний від бізнесу) спосіб. Наприклад, припустимо, що ми хочемо змінити колір тексту залежно від деяких бізнес-умов.

Це можна зробити в три кроки: створити новий віджет, зареєструвати його в реєстрі полів, а потім додати віджет до поля в представленні форми

  • створення нового віджета:

    Це можна зробити, розширивши віджет:

    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);
    
  • додавання віджета в режим представлення форми
    <field name="somefield" widget="my-custom-field"/>
    

    Зауважте, що цей реєстр віджетів полів використовується лише для представлень форм, списків і канбану. Ці подання тісно інтегровані, оскільки представлення списку та канбану можуть відображатися в представленні форми).

Змінення існуючого віджета поля

Інший випадок використання полягає в тому, що ми хочемо змінити існуючий віджет поля. Наприклад, додатку VoIP в odoo потрібно змінити віджет FieldPhone, щоб додати можливість легко зателефонувати на заданий номер через VoIP. Це робиться шляхом включення віджета FieldPhone, тому немає потреби змінювати будь-який існуюче представлення форми.

Віджети полів (екземпляри (підкласу) 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...
        }
    },
});

Зверніть увагу, що немає необхідності додавати віджет до реєстру, оскільки він уже зареєстрований.

Зміна головного віджета з інтерфейсу

Іншим поширеним випадком використання є необхідність налаштувати деякі елементи інтерфейсу користувача. Наприклад, додавання повідомлення в головне меню. Звичайним процесом у цьому випадку знову є включення віджета. Це єдиний спосіб зробити це, оскільки для цих віджетів немає реєстрів.

Зазвичай це робиться за допомогою такого коду:

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) і модель (від 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 і налаштувати інші три класи. Рендерер відповідає за представлення даних в інтерфейсі користувача, Модель має спілкуватися з сервером, завантажувати дані та обробляти їх. А Контролер є для координації, спілкування з веб-клієнтом, …

  • створення деяких представлень у базі даних:
    <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>
    

Налаштування існуючого представлення

Припустімо, що нам потрібно створити спеціальну версію загального представлення. Наприклад, перегляд канбану з деяким додатковим ribbon-like віджетом зверху (для відображення певної спеціальної інформації). У цьому випадку це можна зробити за допомогою 3 кроків: розширити представлення канбану (що також, ймовірно, означає розширення контролерів/рендерів та/або моделей), потім зареєструвати представлення в реєстрі представлень і, нарешті, використовувати представлення в арці канбану (конкретний приклад - інф. панель служби підтримки).

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

    Ось як це може виглядати:

    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 має використовувати наш новий клас. Зауважте, що це стосується конкретного веб-клієнта. З точки зору сервера, ми все ще маємо представлення канбану. Правильний спосіб зробити це - використати спеціальний атрибут 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>
    

Примітка

Примітка: ви можете змінити спосіб інтерпретації структури арки. Однак, з точки зору сервера, це все ще представлення того самого базового типу, яке підпорядковується тим самим правилам (наприклад, перевірка rng). Отже, ваші представлення все ще повинні мати дійсне поле arch.

Обіцянки та асинхронний код

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

Створення нових обіцянок

  • перетворити константу на обіцянку

    У обіцянки є 2 статичні функції, які створюють вирішене або відхилене обіцяння на основі константи:

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

    Примітка

    Зауважте, що навіть якщо обіцянки створено вже вирішеними або відхиленими, обробники then або catch все одно будуть викликатися асинхронно.

  • на основі вже асинхронного коду

    Припустімо, що у функції ви повинні виконати rpc, а після завершення встановити результат для цього. this._rpc - це функція, яка повертає Обіцянка.

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

    Припустімо, що ви використовували функцію this.close, яка приймає як параметр зворотний виклик, який викликається після завершення закриття. Тепер припустімо, що ви робите це в методі, який повинен надіслати обіцянку, яка вирішується після завершення закриття.

    1 function waitForClose() {
    2     var self = this;
    3     return new Promise (function(resolve, reject) {
    4         self.close(resolve);
    5     });
    6 }
    
    • рядок 2: ми зберігаємо this у змінній, щоб у внутрішній функції ми могли отримати доступ до області видимості нашого компонента

    • рядок 3: ми створюємо та повертаємо нову обіцянку. Конструктор обіцянки приймає функцію як параметр. Ця функція сама по собі має 2 параметри, які ми тут назвали resolve і reject
      • resolve - це функція, яка під час виклику переводить обіцянку в стан дозволеного.

      • reject - це функція, яка під час виклику переводить обіцянку у відхилений стан. Ми не використовуємо reject тут, і його можна опустити.

    • рядок 4: ми викликаємо функцію close на нашому об’єкті. Вона приймає функцію як параметр (зворотний виклик), і буває так, що resolve вже є функцією, тому ми можемо передати її безпосередньо. Щоб було зрозуміліше, ми могли б написати:

    return new Promise (function (resolve) {
        self.close(function () {
            resolve();
        });
    });
    
  • створення генератора обіцянок (виклик однієї обіцянки за іншою послідовно і очікування останньої)

    Припустимо, що вам потрібно зробити цикл над масивом, виконати операцію послідовно і обчислити обіцянку, коли буде виконано останню операцію.

    function doStuffOnArray(arr) {
        var done = Promise.resolve();
        arr.forEach(function (item) {
            done = done.then(function () {
                return item.doSomethingAsynchronous();
            });
        });
        return done;
    }
    

    Таким чином, обіцянка, яку ви повертаєте, фактично є останньою обіцянкою.

  • створення обіцянки, а потім вирішення її поза межами її визначення (антипатерн)

    Примітка

    ми не рекомендуємо його використовувати, але іноді він буває корисним. Спочатку добре подумайте про альтернативи…

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

Обробка помилок

  • загалом в обіцянках

    Загальна ідея полягає у тому, що обіцянку не слід відхиляти через потік керування, а слід відхиляти лише через помилки. У такому випадку ви матимете декілька варіантів виконання вашої обіцянки, наприклад, з кодами стану, які вам доведеться перевіряти в обробниках 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 трапляється, що ми використовуємо відхилення обіцянок для потоку керування, як у м’ютексах та інших примітивах паралелізму, визначених у модулі web.concurrency Ми також хочемо виконувати перехоплення з бізнесових причин, але не тоді, коли є помилка кодування у визначенні обіцянки або обробників. Для цього ми ввели поняття 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
    

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

  • використання обіцянок у тестах

    У тестовому коді ми підтримуємо останню версію Javascript, включаючи такі примітиви, як async та await. Це робить використання та очікування обіцянок дуже простим. Більшість допоміжних методів також повертають обіцянку (або з позначкою async, або безпосередньо повертаючи обіцянку).

    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, оскільки це зрозуміліше і коротше для написання.