框架概述

介绍

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

Web 客户端最初是一个使用自定义类和部件系统构建的应用,但现在正在过渡为使用原生 JavaScript 类,并以 Owl 作为组件系统。这就解释了为什么代码库中目前两种系统并存。

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

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

注解

在 Odoo 生态系统中,常常见到用 前端 和 后端 分别作为 Odoo 网站(公开)和 Web 客户端的同义词。不要将此术语与更常见的用法(浏览器代码为前端、服务器为后端)混淆。

注解

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

注解

如有可能,所有新开发都应使用 Owl 完成!

代码结构

web/static/src 文件夹包含 web/ 的全部 JavaScript(以及 CSS 和模板)代码库。以下是最重要的几个文件夹:

  • core/ 大部分底层功能

  • fields/ 所有字段组件

  • views/ 所有 JavaScript 视图组件(form、list 等)

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

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

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

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

WebClient 架构

如上所述,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

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

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

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

注解

持有对环境对象的引用非常有用,因为通过它可以访问所有服务。这在许多情况下都很有帮助:例如,用户菜单项大多定义为一个字符串和一个以 env 为唯一参数的函数。这已足以表达用户菜单的所有需求。

构建模块

Web 客户端的大部分内容由几种抽象构建而成:注册表、服务、组件和钩子。

注册表

注册表 本质上是一种简单的键/值映射,用于存储某类特定对象。它们是 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。

服务

服务 是提供某项功能的长期存在的代码。它们可以被组件(通过 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 组件就是属于 Web 客户端一部分的 Owl 组件。

Hooks 是一种把代码提取复用的方式,即使这些代码依赖生命周期。这是一种可组合/函数式的方式,用于向组件注入某项功能。它们可以看作一种 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

number[]

该用户当前启用的公司 ID 列表

lang

string

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

tz

string

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

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

注解

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

动作上下文

ir.actions.act_window 和 ir.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()) – 一个提供额外求值上下文的对象

退货

any 该表达式在给定上下文下的求值结果

evaluateExpr(expr[, context])
参数
  • expr (string()) – 一个表示有效 Python 表达式的字符串

  • context (Object()) – 一个提供额外求值上下文的对象

退货

any 该表达式在给定上下文下的求值结果

域

广义上说,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()
退货

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') – 一个运算符

退货

域

根据运算符参数的值,返回表示所有域的交集或并集的一个域。

总线

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,例如 location、localStorage 或 setTimeout。例如,下面展示了如何使用 browser.setTimeout 函数:

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

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

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

它包含以下内容:

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 模式的当前值可在 环境 中读取:env.debug。

小技巧

要在调试模式下仅显示菜单、字段或视图元素,应将目标设为用户组 base.group_no_one:

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

更多内容

资源模式

debug=assets 子模式有助于调试 JavaScript 代码:激活后,资源包 不再被压缩,同时还会生成 source map。因此它适用于调试各种 JavaScript 代码。

测试模式

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

更多内容

工具提示

由于 Web 客户端运行在浏览器中,可以使用常规的 title 属性来显示工具提示。然而,这种方式有以下缺点:这些工具提示无法设置样式,必须是纯文本,也无法以任何方式定制(例如为它们增加显示和关闭的延迟,或调整其位置)。

为解决这些问题,Odoo Web 框架提供了一种使用特殊 data 属性显示工具提示的标准方式:

<button data-tooltip="This is a tooltip">Do something</button>

仅当这种提示能为用户带来额外信息时才会显示:如果其内容与元素显示的文本完全相同,且该文本完整可见(即既未被溢出截断,也未被祖先元素裁剪),那么提示只会重复用户已经能读到的东西,因此不会显示。这通常用于被截断的文本,以便用户读取其完整值:

<span class="text-truncate" t-att-data-tooltip="record.name">
  <t t-out="record.name"/>
</span>

请注意,这不适用于使用模板的提示(见下文),因为它们的内容无法与文本进行比较。

工具提示的理想位置可通过 data-tooltip-position 属性指定。它应为以下字符串之一:top、bottom、left 或 right。打开延迟(以毫秒为单位)可通过 data-tooltip-delay 属性修改(默认:400)。

<button data-tooltip="This is a tooltip" data-tooltip-position="left" data-tooltip-delay="0">
  Do something
</button>

对于包含动态内容和/或 HTML 内容的高级工具提示,可以使用 data-tooltip-template 和 data-tooltip-info 属性。例如,假设我们有如下 QWeb 模板:

<t t-name="some_template">
  <ul>
    <li>info.x</li>
    <li>info.y</li>
  </ul>
</t>

然后可以按如下方式在工具提示中使用此模板:

<button data-tooltip-template="some_template" data-tooltip-info="info">
  Do something
</button>

其中 info 是一个包含两个键 x 和 y 的字符串化对象。