框架概览

简介

Odoo Javascript 框架是 web/ 插件提供的一组特性和构建模块,用于帮助构建在浏览器中运行的 odoo 应用。同时,Odoo Javascript 框架本身也是一个单页应用,通常称为 web client (可通过 /web 地址访问)。

web client 起初是使用自定义的类和组件(widget)系统构建的应用,但现在正逐步转为使用原生 javascript 类以及 Owl 作为组件系统。这也解释了为什么当前代码库中两套系统并存。

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

javascript 框架(全部或部分)也用于其他场景,例如 Odoo 网站或零售点(point of sale)。本参考文档主要聚焦于 web client。

Note

在 Odoo 生态中,frontend 和 backend 这两个词常被分别用作 odoo 网站(公开的)和 web client 的同义词。请不要将这一术语与更常见的用法(浏览器代码为 frontend、服务器为 backend)混淆。

Note

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

Note

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

代码结构

web/static/src 文件夹包含 web/ 的全部 javascript(以及 css 和模板)代码。以下是最重要的一些文件夹:

  • core/ 大部分底层特性

  • fields/ 所有字段组件

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

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

  • webclient/ web client 专属代码:导航栏、用户菜单、action 服务等

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

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

WebClient 架构

如上所述,web client 是一个 owl 应用。下面是其模板的一个略微简化的版本:

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

可以看到,它基本上是一个包装器,包含一个导航栏、当前动作以及一些额外的组件。ActionContainer 是一个高阶组件,用于显示当前的 action 控制器(即客户端动作,或在 act_window 类型动作的情况下是某个特定视图)。管理动作是其工作的重要部分:action 服务会在内存中维护一个所有活动动作的栈(显示在面包屑中),并协调每一次变更。

另一个值得注意的有趣之处是 MainComponentsContainer:它只是一个显示 main_components 注册表中所有已注册组件的组件。系统的其他部分正是通过这种方式来扩展 web client 的。

环境

作为一个 Owl 应用,Odoo web client 定义了自己的环境(组件可以通过 this.env 访问它)。以下是 Odoo 在共享的 env 对象上添加的内容说明:

键

值

qweb

Owl 所必需(包含所有模板)

bus

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

services

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

debug

字符串。如果非空,则 web client 处于 调试模式

_t

翻译函数

isSmall

boolean。若为 true,表示 Web 客户端当前处于移动模式(屏幕宽度 <= 767px)

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

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

Note

持有环境的引用非常强大,因为它可以访问所有服务。这在很多情况下都很有用:例如,用户菜单项大多定义为一个字符串和一个以 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 组件。

钩子 是一种代码复用(factorize)的方式,即使代码依赖于生命周期。这是一种可组合的、函数式地向组件注入功能的方法。它们可以被视为一种混入。

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 中一个重要的概念是 context (上下文):它为代码提供了一种方式,可以在函数调用或 rpc 中传递更多上下文信息,使系统的其他部分能够对该信息做出正确的反应。在某种意义上,它就像一个传播到各处的信息包。它在某些情况下很有用,例如让 Odoo 服务器知道某个模型 rpc 来自特定的表单视图,或在组件中启用/禁用某些功能。

Odoo Web 客户端中有两种不同的上下文:用户上下文 和 动作上下文 (因此,使用 context 这个词时要小心:它的含义取决于具体情境)。

Note

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 服务会自动将用户上下文添加到它的每个请求中。这就是为什么在大多数情况下通常无需直接导入它。

Note

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 编写的修饰符(modifier),但它们需要由浏览器来求值。

示例:

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()) – 要分词(tokenize)的表达式

返回

Token[] token 列表

parse(tokens)
参数
  • tokens (Token[]()) – 一个 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()) – 记录对象

返回

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[]) – 一个域表示列表

返回

Domain

返回表示所有域交集的域。

static Domain.or(domains)
参数

domains (string[] | any[][] | Domain[]) – 一个域表示列表

返回

Domain

返回表示所有域并集的域。

static Domain.not(domain)
参数

domain (string | any[] | Domain) – 一个域表示

返回

Domain

返回表示参数域取反的域

static Domain.combine(domains, operator)
参数
  • domains (string[] | any[][] | Domain[]) – 一个域表示列表

  • operator ('AND' or 'OR') – 一个运算符

返回

Domain

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

总线

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

无

菜单服务(menu service)的当前应用已更改

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

它主要用于测试目的:所有使用 browser 对象的代码都可以通过在测试期间模拟(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。

Tip

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

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

更多内容

资源模式

debug=assets 子模式对于调试 javascript 代码很有用:一旦激活,资源 包将不再被压缩,并且还会生成 source-map。这使得它适合调试各种类型的 javascript 代码。

测试模式

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

更多内容