框架概览

简介

Odoo JavaScript 框架是一组由 web/ 插件提供的功能/构建块,用于帮助构建在浏览器中运行的 Odoo 应用程序。同时,Odoo JavaScript 框架是一个单页应用程序,通常称为 Web 客户端 (可通过 URL /web 访问)。

Web 客户端最初是使用自定义类和小部件系统构建的应用程序,但现在正逐步过渡到使用原生 JavaScript 类,并采用 Owl 作为组件系统。这解释了为什么当前代码库中同时使用了这两种系统。

从高层视角来看,Web 客户端是一个单页应用程序:它不需要在用户每次执行操作时向服务器请求完整页面,而是只请求所需内容,并相应地替换/更新当前屏幕。此外,它还会管理 URL,以使其与当前状态保持同步。

JavaScript 框架(全部或部分)也用于其他场景,例如 Odoo 网站或销售点。本文档主要聚焦于 Web 客户端。

注解

在 Odoo 生态系统中,常见将 frontendbackend 分别视为 Odoo 网站(公共部分)和 Web 客户端的同义词。此术语不应与更常见的浏览器代码(前端)和服务器(后端)混淆。

注解

在本文档中,component 始终指新的 Owl 组件,而 widget 指旧的 Odoo 小部件。

注解

所有新开发应尽可能基于 Owl 进行!

代码结构

web/static/src 文件夹包含所有 web/ 的 JavaScript(以及 CSS 和模板)代码库。以下是一些最重要的文件夹列表:

  • core/ 大多数低级功能

  • fields/ 所有字段组件

  • views/ 所有 JavaScript 视图组件(formlist 等)

  • search/ 控制面板、搜索栏、搜索面板等

  • webclient/ Web 客户端特定代码:导航栏、用户菜单、动作服务等

web/static/src 是根文件夹。内部的所有内容都可以通过使用 @web 前缀简单导入。例如,以下是如何导入位于 web/static/src/core/utils/functions 中的 memoize 函数:

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

Web 客户端架构

如前所述,Web 客户端是一个 Owl 应用程序。以下是其模板的简化版本:

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

可以看到,它基本上是一个导航栏、当前动作和一些附加组件的包装器。ActionContainer 是一个高阶组件,用于显示当前动作控制器(例如,客户端动作,或者对于类型为 act_window 的动作,显示特定视图)。管理动作是其工作的重要部分:动作服务会将所有活动动作的堆栈保存在内存中(在面包屑中表示),并协调每个更改。

另一个值得注意的有趣之处是 MainComponentsContainer:它是一个简单的组件,用于显示在 main_components 注册表中注册的所有组件。这就是系统其他部分如何扩展 Web 客户端的方式。

环境

作为一个 Owl 应用程序,Odoo Web 客户端定义了自己的环境(组件可以通过 this.env 访问它)。以下是 Odoo 添加到共享 env 对象中的内容描述:

qweb

Owl 所需(包含所有模板)

bus总线

主总线,用于协调一些通用事件

services服务

所有已部署的 服务 (通常应通过 useService 钩子访问)

debug调试

字符串。如果非空,则 Web 客户端处于 调试模式

_t

翻译函数

isSmall

布尔值。如果为真,则 Web 客户端当前处于移动模式(屏幕宽度 <= 767px)

例如,要在组件中翻译字符串(注意:模板会自动翻译,因此在这种情况下无需特定操作),可以这样做:

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

注解

拥有对环境的引用非常强大,因为它提供了对所有服务的访问权限。这在许多情况下都很有用:例如,用户菜单项大多被定义为字符串,以及一个以 env 作为唯一参数的函数。这就足以表达所有用户菜单的需求。

构建块

Web 客户端的大部分是通过几种抽象类型构建的:注册表、服务、组件和钩子。

Registries注册表

注册表 基本上是一个简单的键/值映射,用于存储某种特定类型的对象。它们是 UI 可扩展性的重要组成部分:一旦某个对象被注册,Web 客户端的其余部分就可以使用它。例如,字段注册表包含视图中可以使用的所有字段组件(或小部件)。

import { Component } from "@odoo/owl";
import { registry } from "./core/registry";

class MyFieldChar extends Component {
    // some code
}

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

请注意,我们从 @web/core/registry 导入主注册表,然后打开子注册表 fields

Services服务

服务 是提供某些功能的长期运行代码片段。它们可以被组件(通过 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);

Components组件和Hooks钩子

组件钩子 的概念来源于 Owl 组件系统。Odoo 组件只是属于 Web 客户端的 Owl 组件。

钩子 是一种代码分解方式,即使代码依赖于生命周期也是如此。这是一种将功能注入组件的可组合/函数式方法。它们可以被视为一种混入(mixin)。

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

上下文

Odoo JavaScript 中的一个重要概念是 上下文:它为代码提供了一种方式,为函数调用或 RPC 提供更多上下文信息,以便系统的其他部分能够正确响应这些信息。在某种程度上,它就像一个传播到各处的信息包。它在某些情况下很有用,例如让 Odoo 服务器知道某个模型 RPC 来自特定的表单视图,或者在组件中激活/禁用某些功能。

在 Odoo Web 客户端中有两种不同的上下文: 用户上下文动作上下文 (因此,在使用“上下文”一词时需要小心:根据情况不同,它可能有不同的含义)。

注解

context 对象在许多情况下可能很有用,但应避免过度使用!许多问题可以通过标准方式解决,而无需修改上下文。

用户上下文

用户上下文 是一个小型对象,包含与当前用户相关的各种信息。它可以通过 user 服务访问:

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

它包含以下信息:

名称

类型

描述

allowed_company_ids

数字数组

用户的活动公司 ID 列表

lang

字符串

用户语言代码(例如 “en_us”)

tz

字符串

用户当前时区(例如 “Europe/Brussels”)

实际上,orm 服务会自动将用户上下文添加到其每个请求中。这就是为什么在大多数情况下通常不需要直接导入它的原因。

注解

allowed_company_ids 的第一个元素是用户的主要公司。

Action Context动作上下文

ir.actions.act_windowir.actions.client 支持一个可选的 context 字段。该字段是一个表示对象的 char 类型值。每当相应的动作在 Web 客户端中加载时,此上下文字段将被评估为一个对象,并传递给与该动作对应的组件。

<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

py JavaScript 代码导出了 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 表达式和未评估的值,这些值依赖于某些评估上下文。然而,操作字符串表达式更加困难。

由于域在 Web 客户端中非常重要,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()) – 一个记录对象

返回

布尔值

如果记录符合域中指定的所有条件,则返回 true

Domain.toString()
返回

字符串

返回域的字符串描述

Domain.toList([context])
参数
  • context (Object()) – 评估上下文

返回

任意数组

返回域的列表描述。注意,此方法接受一个可选的 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总线

Web 客户端的 环境 对象包含一个名为 bus 的事件总线。其目的是允许系统各部分在不耦合的情况下正确协调。env.bus 是一个 Owl EventBus,应将其用于感兴趣的全局事件。

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

以下是可以在该总线上触发的事件列表:

消息

负载

触发条件

ACTION_MANAGER:UI-UPDATED

指示 UI 更新部分的模式(’current’、’new’ 或 ‘fullscreen’)

请求动作管理器渲染的动作已完成

ACTION_MANAGER:UPDATE

下一个渲染信息

动作管理器已完成计算下一个界面

MENUS:APP-CHANGED

菜单服务的当前应用程序已更改

ROUTE_CHANGE

URL 哈希已更改

RPC:REQUEST

rpc ID

一个 RPC 请求刚刚开始

RPC:RESPONSE

rpc ID

一个 RPC 请求已完成

WEB_CLIENT_READY

Web 客户端已挂载

FOCUS-VIEW

主视图应聚焦自身

CLEAR-CACHES

所有内部缓存应被清除

CLEAR-UNCOMMITTED-CHANGES

函数列表

所有包含未提交更改的视图应清除这些更改,并将回调推送到列表中

浏览器对象

JavaScript 框架还提供了一个特殊的对象 browser,它提供了对许多浏览器 API 的访问,例如 locationlocalStoragesetTimeout。例如,以下是如何使用 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 模式未激活,否则它处于激活状态。如果字符串包含 assetstests,则相应的特定子模式会被激活(见下文)。两种模式可以同时激活,例如使用字符串 assets,tests

debug 模式的当前值可以通过 环境 读取:env.debug

小技巧

要在调试模式下仅显示菜单、字段或视图元素,您应该针对组 base.group_no_one

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

资源模式

debug=assets 子模式对于调试 JavaScript 代码非常有用:一旦激活,assets 包将不再被压缩,并且还会生成源映射。这使得调试各种 JavaScript 代码变得更加方便。

测试模式

还有另一个名为 tests 的子模式:如果启用,服务器会在页面中注入 web.assets_tests 包。该包主要包含测试游览(其目的是测试功能,而不是向用户展示有趣的内容)。因此,tests 模式对于运行这些游览非常有用。

参见