框架概述

介绍

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

Web客户端最初是使用自定义类和小部件系统创建的应用程序,但现在正在过渡到使用本地JavaScript类和Owl作为组件系统。这就解释了为什么代码库中目前同时使用了这两个系统。

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

JavaScript 框架(全部或部分)也用于其他情况,例如 Odoo 网站或销售点。本参考文档主要关注于 Web 客户端。

注解

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

注解

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

注解

如有可能,所有新的开发工作应该使用Owl完成!

代码结构

The web/static/src folder contains all the web/ javascript (and css and templates) codebase. Here is a list of the most important folders:

  • core/ most of the low level features

  • fields/ all field components

  • views/ 所有 JavaScript 视图组件 (form, list, …)

  • search/ control panel, search bar, search panel, …

  • webclient/ the web client specific code: navbar, user menu, action service, …

The web/static/src is the root folder. Everything inside can simply be imported by using the @web prefix. For example, here is how one can import the memoize function located in web/static/src/core/utils/functions:

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

main bus, used to coordinate some generic events

services

所有已部署的 services (通常应使用 useService hook 访问)

debug

string。如果非空,则 web 客户端处于 debug mode

_t

翻译函数

isSmall

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

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

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

注解

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

构建块

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

注册表

Registries are basically a simple key/value mapping that stores some specific kind of objects. They are an important part of the extensibility of the UI: once some object is registered, the rest of the web client can use it. For example, the field registry contains all field components (or widgets) that can be used in views.

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

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

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

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

服务

Services are long lived pieces of code that provide a feature. They may be imported by components (with useService) or by other services. Also, they can declare a set of dependencies. In that sense, services are basically a DI (dependency injection) system. For example, the notification service provides a way to display a notification, or the rpc service is the proper way to perform a request to the Odoo server.

以下示例注册了一个简单的服务,每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 are a way to factorize code, even if it depends on lifecycle. This is a composable/functional way to inject a feature in a component. They can be seen as a kind of 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中, context 是一个重要的概念:它为代码提供了一种方式,可以给函数调用或rpc提供更多的上下文信息,以便系统的其他部分可以正确地对这些信息做出反应。在某种程度上,它就像是一个传播到各处的信息包。在某些情况下非常有用,比如让Odoo服务器知道模型rpc来自特定的表单视图,或者在组件中激活/禁用某些功能。

在Odoo web客户端中有两个不同的上下文: 用户上下文动作上下文 (所以,在使用 上下文 这个词时要小心:它可能在不同的情况下有不同的含义)。

注解

The context object may be useful in many cases, but one should be careful not to overuse it! Many problems can be solved in a standard way without modifying the context.

用户上下文

The user context is a small object containing various informations related to the current user. It is available through the user service:

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

它包含以下信息:

名称

类型

描述

allowed_company_ids

number[]

用户的活动公司ID列表

lang

string

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

tz

string

用户当前时区(例如“欧洲/布鲁塞尔”)

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

注解

The first element of the allowed_company_ids is the main company of the user.

操作上下文

The ir.actions.act_window and ir.actions.client support an optional context field. This field is a char that represents an object. Whenever the corresponding action is loaded in the web client, this context field will be evaluated as an object and given to the component that corresponds to the action.

<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

The py javascript code exports 5 functions:

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()) – 评估上下文

返回

any[]

返回域的列表描述。请注意,此方法接受一个可选的 context 对象,该对象将用于替换所有自由变量。

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

The Domain class also provides 4 useful static methods to combine domains:

// ["&", ("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') – 一个客服人员

返回

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

总线

网页客户端 环境 对象包含一个名为 bus 的事件总线。它的目的是允许系统的各个部分适当地协调自己,而不耦合它们。 env.bus 是一个 owl EventBus <https://github.com/odoo/owl/blob/master/doc/reference/event_bus.md> _,应该用于全局感兴趣的事件。

// 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 的访问权限,比如 locationlocalStorage 或者 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界面。

The debug mode is described by a string. An empty string means that the debug mode is not active. Otherwise, it is active. If the string contains assets or tests, then the corresponding specific sub modes are activated (see below). Both modes can be active at the same time, for example with the string assets,tests.

The debug mode current value can be read in the 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 模式对于能够运行这些游览非常有用。

另请参阅