Огляд фреймворку

Вступ

Фреймворк Odoo Javascript - це набір функцій/структурних блоків, що надаються доповненням web/, щоб допомогти створювати застосунки odoo, що працюють у браузері. Водночас, фреймворк Odoo Javascript - це односторінковий застосунок, зазвичай відомий як веб-клієнт (доступний за URL-адресою /web).

Веб-клієнт починався як застосунок, створений з використанням власної системи класів та віджетів, але зараз він переходить на використання нативних класів JavaScript та Owl як системи компонентів. Це пояснює, чому обидві системи зараз використовуються в кодовій базі.

З точки зору загальних принципів, веб-клієнт - це односторінковий застосунок: йому не потрібно запитувати повну сторінку від сервера щоразу, коли користувач виконує дію. Натомість він запитує лише те, що йому потрібно, а потім відповідно замінює/оновлює поточний екран. Крім того, він керує URL-адресою, щоб синхронізувати її з поточним станом.

Фреймворк javascript (повністю або частково) також використовується в інших ситуаціях, таких як вебсайт Odoo або точка продажу. Це посилання здебільшого стосується веб-клієнта.

Примітка

В екосистемі Odoo слова frontend та backend часто використовуються як синоніми веб-сайту odoo (публічного) та веб-клієнта відповідно. Цю термінологію не слід плутати з більш поширеним використанням термінів browser-code (frontend) та server (backend).

Примітка

У цій документації слово компонент завжди стосується нових компонентів Owl, а віджет - старих віджетів Odoo.

Примітка

Всю нову розробку, якщо це можливо, слід виконувати в Owl!

Структура коду

Папка web/static/src містить всю кодову базу javascript (а також css та шаблони) web/. Ось список найважливіших папок:

  • core/ більшість низькорівневих функцій

  • fields/ усі компоненти поля

  • views/ усі компоненти представлень JavaScript (form, list, …)

  • панель керування search/, рядок пошуку, панель пошуку, …

  • webclient/ код, специфічний для веб-клієнта: панель навігації, меню користувача, служба дій тощо, …

web/static/src - це коренева папка. Все, що знаходиться всередині, можна просто імпортувати, використовуючи префікс @web. Наприклад, ось як можна імпортувати функцію memoize, розташовану в web/static/src/core/utils/functions:

import { memoize } from "@web/core/utils/functions";

Архітектура WebClient

Як згадувалося вище, веб-клієнт - це застосунок у стилі owl. Ось дещо спрощена версія його шаблону:

<t t-name="web.WebClient">
    <body class="o_web_client">
        <NavBar/>
        <ActionContainer/>
        <MainComponentsContainer/>
    </body>
</t>

Як ми бачимо, це, по суті, обгортка для панелі навігації, поточної дії та деяких додаткових компонентів. ActionContainer - це компонент вищого порядку, який відображатиме поточний контролер дій (тобто дію клієнта або певний вигляд у випадку дій типу act_window). Керування діями є величезною частиною його роботи: служба дій зберігає в пам’яті стек усіх активних дій (представлених у навігаційних сухарях) та координує кожну зміну.

Ще одна цікава річ, яку варто відзначити, це MainComponentsContainer: це просто компонент, який відображає всі компоненти, зареєстровані в реєстрі main_components. Таким чином інші частини системи можуть розширювати веб-клієнт.

Середовище

Як застосунок Owl, веб-клієнт Odoo визначає власне середовище (компоненти можуть отримати до нього доступ за допомогою this.env). Ось опис того, що Odoo додає до спільного об’єкта env:

Key

Значення

qweb

вимагається Owl (містить усі шаблони)

bus

main bus, використовується для координації деяких загальних подій

services

усі розгорнуті services (зазвичай слід отримувати доступ за допомогою гачка useService)

debug

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

_t

функція перекладу

isSmall

логічне значення. Якщо значення true, веб-клієнт наразі перебуває в мобільному режимі (ширина екрана <= 767px)

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

const someString = this.env._t('some text');

Примітка

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

Будівельні блоки

Більшість веб-клієнтів побудовано з кількома типами абстракцій: реєстри, сервіси, компоненти та перехоплювачі.

Реєстри

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

import { registry } from "./core/registry";

class MyFieldChar extends owl.Component {
    // some code
}

registry.category("fields").add("my_field_char", MyFieldChar);

Зверніть увагу, що ми імпортуємо основний реєстр з @web/core/registry, а потім відкриваємо fields підреєстру.

Послуги

Сервіси - це довговічні фрагменти коду, які надають певну функцію. Вони можуть бути імпортовані компонентами (за допомогою useService) або іншими сервісами. Також вони можуть оголошувати набір залежностей. У цьому сенсі сервіси є, по суті, системою DI (впровадження залежностей). Наприклад, сервіс notification надає спосіб відображення сповіщення, або сервіс rpc - це правильний спосіб виконання запиту до сервера Odoo.

У наступному прикладі реєструється простий сервіс, який відображає сповіщення кожні 5 секунд:

import { registry } from "./core/registry";

const serviceRegistry = registry.category("services");

const myService = {
    dependencies: ["notification"],
    start(env, { notification }) {
        let counter = 1;
        setInterval(() => {
            notification.add(`Tick Tock ${counter++}`);
        }, 5000);
    }
};

serviceRegistry.add("myService", myService);

Компоненти та Hooks

Компоненти та hooks – це ідеї, що походять із системи компонентів Owl. Компоненти Odoo – це просто компоненти owl, які є частиною веб-клієнта.

Hooks – це спосіб факторизації коду, навіть якщо це залежить від життєвого циклу. Це компонований/функціональний спосіб впровадження функції в компонент. Їх можна розглядати як різновид міксину.

function useCurrentTime() {
    const state = useState({ now: new Date() });
    const update = () => state.now = new Date();
    let timer;
    onWillStart(() => timer = setInterval(update, 1000));
    onWillUnmount(() => clearInterval(timer));
    return state;
}

Контекст

Важливою концепцією в javascript Odoo є контекст: він надає коду можливість надати більше контексту виклику функції або rpc, щоб інші частини системи могли належним чином реагувати на цю інформацію. У певному сенсі це схоже на мішок інформації, який поширюється всюди. Це корисно в деяких ситуаціях, наприклад, для повідомлення серверу Odoo про те, що модель rpc походить з певного вигляду форми, або для активації/вимикання деяких функцій у компоненті.

У веб-клієнті Odoo є два різних контексти: контекст користувача та контекст дії (тому слід бути обережними, використовуючи слово контекст: воно може означати різне залежно від ситуації).

Примітка

Об’єкт context може бути корисним у багатьох випадках, але слід бути обережним і не зловживати ним! Багато проблем можна вирішити стандартним способом без зміни контексту.

Контекст користувача

Контекст користувача - це невеликий об’єкт, що містить різноманітну інформацію, пов’язану з поточним користувачем. Він доступний через сервіс user:

class MyComponent extends Component {
    setup() {
        const user = useService("user");
        console.log(user.context);
    }
}

Він містить таку інформацію:

Name

Тип

Опис

allowed_company_ids

number[]

список активних ідентифікаторів компаній для користувача

lang

string

код мови користувача (наприклад, «en_us»)

tz

string

поточний часовий пояс користувача (наприклад, «Europe/Brussels»)

На практиці, сервіс orm автоматично додає контекст користувача до кожного свого запиту. Саме тому в більшості випадків його безпосередньо імпортувати не потрібно.

Примітка

Перший елемент allowed_company_ids – це основна компанія користувача.

Контекст дії

ir.actions.act_window та ir.actions.client підтримують необов’язкове поле context. Це поле є char, що представляє об’єкт. Щоразу, коли відповідна дія завантажується у веб-клієнт, це поле контексту буде оцінено як об’єкт і передано компоненту, що відповідає дії.

<field name="context">{'search_default_customer': 1}</field>

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

Іноді, коли ми виконуємо нові дії вручну (тобто програмно, у javascript), корисно мати можливість розширити контекст дії. Це можна зробити за допомогою аргументу additional_context.

// in setup
let actionService = useService("action");

// in some event handler
actionService.doAction("addon_name.something", {
    additional_context:{
        default_period_id: defaultPeriodId
    }
});

У цьому прикладі буде завантажено дію з xml_id addon_name.something, а її контекст буде розширено значенням default_period_id. Це дуже важливий варіант використання, який дозволяє розробникам поєднувати дії разом, надаючи певну інформацію наступній дії.

Інтерпретатор Python

Фреймворк Odoo має вбудований невеликий інтерпретатор Python. Його призначення - обчислювати невеликі вирази Python. Це важливо, оскільки представлення в Odoo мають модифікатори, написані на Python, але їх має обчислювати браузер.

Приклад:

import { evaluateExpr } from "@web/core/py_js/py";

evaluateExpr("1 + 2*{'a': 1}.get('b', 54) + v", { v: 33 }); // returns 142

JavaScript-код py експортує 5 функцій:

tokenize(expr)
Аргументи
  • expr (string()) – вираз для токенізації

Повертає

Token[] список токенів

parse(tokens)
Аргументи
  • tokens (Token[]()) – список токенів

Повертає

AST - абстрактна структура синтаксичного дерева, що представляє вираз

parseExpr(expr)
Аргументи
  • expr (string()) – рядок, що представляє дійсний вираз Python

Повертає

AST - абстрактна структура синтаксичного дерева, що представляє вираз

evaluate(ast[, context])
Аргументи
  • ast (AST()) – структура AST, що представляє вираз

  • context (Object()) – об’єкт, що забезпечує додатковий контекст оцінювання

Повертає

будь-яке результуюче значення виразу, з урахуванням контексту

evaluateExpr(expr[, context])
Аргументи
  • expr (string()) – рядок, що представляє дійсний вираз Python

  • context (Object()) – об’єкт, що забезпечує додатковий контекст оцінювання

Повертає

будь-яке результуюче значення виразу, з урахуванням контексту

Домени

Загалом кажучи, домени в Odoo представляють собою набір записів, що відповідають деяким заданим умовам. У javascript вони зазвичай представлені або як список умов (або операторів: |, & або ! у префіксній нотації), або як рядкові вирази. Вони не обов’язково повинні бути нормалізованими (оператор & мається на увазі, якщо необхідно). Наприклад:

// list of conditions
[]
[["a", "=", 3]]
[["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]
["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]
["&", "!", ["a", "=", 1], "|", ["a", "=", 2], ["a", "=", 3]]

// string expressions
"[('some_file', '>', a)]"
"[('date','>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"
"[('date', '!=', False)]"

Рядкові вирази потужніші за спискові: вони можуть містити вирази Python та неоцінені значення, що залежить від певного контексту оцінювання. Однак маніпулювання рядковими виразами є складнішим.

Оскільки домени є досить важливими у веб-клієнті, Odoo надає клас Domain:

new Domain([["a", "=", 3]]).contains({ a: 3 }) // true

const domain = new Domain(["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]);
domain.contains({ a: 1, b: 2, c: 3 }); // true
domain.contains({ a: -1, b: 2, c: 3 }); // false

// next expression returns ["|", ("a", "=", 1), ("b", "<=", 3)]
Domain.or([[["a", "=", 1]], "[('b', '<=', 3)]"]).toString();

Ось опис класу Domain:

class Domain([descr])
Аргументи
  • descr (string | any[] | Domain()) – опис домену

Domain.contains(record)
Аргументи
  • record (Object()) – об’єкт запису

Повертає

boolean

Повертає значення true, якщо запис відповідає всім умовам, визначеним доменом

Domain.toString()
Повертає

string

Повертає рядковий опис домену

Domain.toList([context])
Аргументи
  • context (Object()) – контекст оцінювання

Повертає

any[]

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

new Domain(`[('a', '>', b)]`).toList({ b:3 }); // [['a', '>', 3]]

Клас Domain також надає 4 корисні статичні методи для об’єднання доменів:

// ["&", ("a", "=", 1), ("uid", "<=", uid)]
Domain.and([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString();

// ["|", ("a", "=", 1), ("uid", "<=", uid)]
Domain.or([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString();

// ["!", ("a", "=", 1)]
Domain.not([["a", "=", 1]]).toString();

// ["&", ("a", "=", 1), ("uid", "<=", uid)]
Domain.combine([[["a", "=", 1]], "[('uid', '<=', uid)]"], "AND").toString();
static Domain.and(domains)
Параметри

domains (string[] | any[][] | Domain[]) – список представлень доменів

Повертає

Домен

Повертає домен, що представляє собою перетин усіх доменів.

static Domain.or(domains)
Параметри

domains (string[] | any[][] | Domain[]) – список представлень доменів

Повертає

Домен

Повертає домен, що представляє об’єднання всіх доменів.

static Domain.not(domain)
Параметри

domain (string | any[] | Domain) – представлення домену

Повертає

Домен

Повертає домен, що представляє заперечення аргумента домену

static Domain.combine(domains, operator)
Параметри
  • domains (string[] | any[][] | Domain[]) – список представлень доменів

  • operator ('AND' or 'OR') – оператор

Повертає

Домен

Повертає домен, що представляє або перетин, або об’єднання всіх доменів, залежно від значення аргумента оператора.

Bus

Об’єкт веб-клієнта environment містить шину подій під назвою bus. Її призначення - дозволити різним частинам системи належним чином координувати свою роботу, не пов’язуючи їх між собою. env.bus - це шина подій EventBus, яку слід використовувати для глобальних подій, що цікавлять.

// for example, in some service code:
env.bus.on("WEB_CLIENT_READY", null, doSomething);

Ось список подій, які можуть бути ініційовані на цій шині:

Повідомлення

Payload

Тригер

ACTION_MANAGER:UI-UPDATED

режим, що вказує, яка частина інтерфейсу користувача була оновлена („current“, „new“ або „fullscreen“)

виконання запитуваної дії для менеджера дій

ACTION_MANAGER:UPDATE

наступна інформація про рендеринг

менеджер дій завершив обчислення наступного інтерфейсу

MENUS:APP-CHANGED

none

поточний додаток сервісу меню змінився

ROUTE_CHANGE

none

хеш URL-адреси було змінено

RPC:REQUEST

rpc id

щойно розпочато виконання RPC-запиту

RPC:RESPONSE

rpc id

запит RPC виконано

WEB_CLIENT_READY

none

веб-клієнт встановлено

FOCUS-VIEW

none

основне представлення має бути зосереджене на собі

CLEAR-CACHES

none

всі внутрішні кеші слід очистити

CLEAR-UNCOMMITTED-CHANGES

список функцій

усі представлення з незбереженими змінами слід очистити та надіслати зворотний виклик у список

Browser Object

Фреймворк javascript також надає спеціальний об’єкт browser, який забезпечує доступ до багатьох API браузера, таких як location, localStorage або setTimeout. Наприклад, ось як можна використовувати функцію browser.setTimeout:

import { browser } from "@web/core/browser/browser";

// somewhere in code
browser.setTimeout(someFunction, 1000);

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

Він містить наступний вміст:

addEventListener

cancelAnimationFrame

clearInterval

clearTimeout

console

Date

fetch

history

localStorage

location

navigator

open

random

removeEventListener

requestAnimationFrame

sessionStorage

setInterval

setTimeout

XMLHttpRequest

Режим налагодження

Odoo іноді може працювати в спеціальному режимі, який називається режимом debug. Він використовується для двох основних цілей:

  • відображати додаткову інформацію/поля для деяких конкретних екранів,

  • надати деякі додаткові інструменти, які допоможуть розробникам налагоджувати інтерфейс Odoo.

Режим debug описується рядком. Порожній рядок означає, що режим debug неактивний. В іншому випадку він активний. Якщо рядок містить assets або tests, то активуються відповідні підрежими (див. нижче). Обидва режими можуть бути активними одночасно, наприклад, з рядком assets,tests.

Поточне значення режиму debug можна прочитати у environment: env.debug.

Порада

Щоб відображати меню, поля або елементи перегляду лише в режимі налагодження, слід обрати групу base.group_no_one:

<field name="fname" groups="base.group_no_one"/>

Режим активів

Підрежим debug=assets корисний для налагодження коду javascript: після активації пакети assets більше не мініфікуються, а також генеруються карти вихідного коду. Це робить його корисним для налагодження всіх видів коду javascript.

Режим тестів

Існує ще один підрежим під назвою tests: якщо його ввімкнено, сервер вставляє пакет web.assets_tests на сторінку. Цей пакет містить здебільшого тестові тури (тури, метою яких є тестування функції, а не показ чогось цікавого користувачам). Режим tests корисний для запуску цих турів.

Перегляньте також