框架概述

简介

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

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

从高层角度来看,网络客户端是一个单页应用程序:当用户执行操作时,它不需要每次都向服务器请求完整页面。相反,它只请求所需的内容,然后根据需要替换/更新当前屏幕。同时,它还会管理网址,以保持与当前状态同步。

JavaScript 框架(全部或部分)也用于其他场景,例如 Odoo 网站或销售点。本引用主要专注于网页客户端。

注解

在 Odoo 生态系统中,通常会将 frontendbackend 这两个词分别作为 odoo 网站(公开)和网络客户端的同义词。这种术语不应与更常见的浏览器代码(前端)和服务器(后端)的用法混淆。

注解

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

注解

所有新开发应尽可能在 Owl 中完成!

代码结构

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

  • core/ 大部分底层功能

  • fields/ 所有字段组件

  • views/ 所有 JavaScript 视图组件 (表单列表、…)

  • 搜索/ 控制面板,搜索栏,搜索面板,…

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

web/static/src 是根目录。其中的所有内容都可以通过使用 @web 前缀进行导入。例如,以下是如何导入位于 web/static/src/core/utils/functions 中的 memoize 函数:

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 对象所做的扩展说明:

QWeb

由 Owl(包含所有模板)必填

消息总线

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

服务

所有已部署的 服务 <frontend/services>`(通常应通过 `useService 钩子访问)

调试

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

_t

翻译函数

是否为小尺寸

布尔类型。如果为 true,则表示网页客户端当前处于移动模式(屏幕宽度 <= 767px)。

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

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

注解

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

构建块

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

注册表

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

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

组件和钩子

组件钩子 是来自 Owl 组件系统 的概念。Odoo 组件只是作为网页客户端一部分的 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 网页客户端中有两种不同的上下文:用户上下文*动作上下文*(因此,在使用“上下文”这个词时,我们需要小心:根据具体情况,它可能表示不同的含义)。

注解

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

用户上下文

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

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

它包含以下信息:

名称

类型

描述

允许的公司ID

数字[]

用户激活的公司ID列表

语言

字符串

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

时区

字符串

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

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

注解

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

动作上下文

动作 ir.actions.act_windowir.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

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') – 一个操作符

返回

领域

返回一个表示所有域交集或并集的域,具体取决于操作符参数的值。

公交车

Web 客户端 环境 对象包含一个事件总线,名为 bus。其目的是允许系统中的各个部分正确地进行协调,而无需彼此耦合。env.bus 是一个 owl 的 EventBus,应用于系统中感兴趣的全局事件。

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

以下是此总线可以触发的事件列表:

消息

有效负载

触发器

ACTION_MANAGER: UI-更新

一种模式,用于指示用户界面的哪个部分已被更新(’current’、’new’ 或 ‘fullscreen’)

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

ACTION_MANAGER:更新

下一次渲染信息

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

MENUS:应用-已更改

none

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

ROUTE_CHANGE

none

网址哈希值已更改

RPC:请求

rpc id

一个 RPC 请求刚刚开始

RPC: 响应

rpc id

一个 RPC 请求已完成

WEB 客户端已就绪

none

网页客户端已挂载

聚焦视图

none

主要视图应自行聚焦

清除缓存

none

所有内部缓存应被清除

清除未提交的更改

函数列表

所有带有未保存更改的视图应清除这些更改,并在列表中推送一个回调函数

浏览器对象

JavaScript 框架还提供了一个特殊的对象 browser,它提供了对许多浏览器 API 的访问,例如 locationlocalStoragesetTimeout。例如,以下是使用 browser.setTimeout 函数的方式:

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

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

它主要用于测试目的:所有使用浏览器对象的代码都可以通过在测试期间模拟相关函数来轻松进行测试。

它包含以下内容:

addEventListener

取消动画帧

清除间隔

清除定时器

控制台

日期

获取

历史

本地存储

库位

导航器

打开

随机

removeEventListener

requestAnimationFrame

sessionStorage

setInterval

setTimeout

XMLHttpRequest

调试模式

Odoo 有时可以运行在一种称为 调试 模式(debug mode)的特殊模式下。它主要用于两个主要目的:

  • 在某些特定屏幕中显示额外的信息/字段,

  • 为开发者提供一些额外的工具,以帮助调试 Odoo 界面。

debug 模式由一个字符串描述。空字符串表示 debug 模式未激活。否则,表示已激活。如果该字符串包含 assetstests,则会激活相应的特定子模式(见下文)。这两种模式可以同时激活,例如字符串为 assets,tests 时。

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

小技巧

要仅在调试模式下显示菜单、字段或视图元素,您应针对用户组 base.group_no_one 进行设置:

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

另请参见

资产模式

debug=assets 子模式用于调试 JavaScript 代码:一旦启用,资源 打包将不再被压缩,并且会生成源映射。这使得调试各种 JavaScript 代码变得非常有用。

测试模式

另一个子模式名为 tests:如果启用,服务器会将包 web.assets_tests 注入到页面中。该包主要包含测试流程(用于测试功能的流程,而不是向用户展示有趣内容的流程)。因此,启用 tests 模式可以运行这些流程。

另请参见