框架概览¶
简介¶
Odoo JavaScript 框架是一组由 web/
插件提供的功能/构建块,用于帮助构建在浏览器中运行的 Odoo 应用程序。同时,Odoo JavaScript 框架是一个单页应用程序,通常称为 Web 客户端 (可通过 URL /web
访问)。
Web 客户端最初是使用自定义类和小部件系统构建的应用程序,但现在正逐步过渡到使用原生 JavaScript 类,并采用 Owl 作为组件系统。这解释了为什么当前代码库中同时使用了这两种系统。
从高层视角来看,Web 客户端是一个单页应用程序:它不需要在用户每次执行操作时向服务器请求完整页面,而是只请求所需内容,并相应地替换/更新当前屏幕。此外,它还会管理 URL,以使其与当前状态保持同步。
JavaScript 框架(全部或部分)也用于其他场景,例如 Odoo 网站或销售点。本文档主要聚焦于 Web 客户端。
注解
在 Odoo 生态系统中,常见将 frontend 和 backend 分别视为 Odoo 网站(公共部分)和 Web 客户端的同义词。此术语不应与更常见的浏览器代码(前端)和服务器(后端)混淆。
注解
在本文档中,component 始终指新的 Owl 组件,而 widget 指旧的 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";
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
对象中的内容描述:
键 |
值 |
---|---|
|
Owl 所需(包含所有模板) |
|
主总线,用于协调一些通用事件 |
|
所有已部署的 服务 (通常应通过 |
|
字符串。如果非空,则 Web 客户端处于 调试模式 |
|
翻译函数 |
|
布尔值。如果为真,则 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);
}
}
它包含以下信息:
名称 |
类型 |
描述 |
---|---|---|
|
|
用户的活动公司 ID 列表 |
|
|
用户语言代码(例如 “en_us”) |
|
|
用户当前时区(例如 “Europe/Brussels”) |
实际上,orm
服务会自动将用户上下文添加到其每个请求中。这就是为什么在大多数情况下通常不需要直接导入它的原因。
注解
allowed_company_ids
的第一个元素是用户的主要公司。
Action Context动作上下文¶
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()
) – 一个提供额外评估上下文的对象
- 返回
任何相对于上下文的表达式结果值
- 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);
以下是可以在该总线上触发的事件列表:
消息 |
负载 |
触发条件 |
---|---|---|
|
指示 UI 更新部分的模式(’current’、’new’ 或 ‘fullscreen’) |
请求动作管理器渲染的动作已完成 |
|
下一个渲染信息 |
动作管理器已完成计算下一个界面 |
|
无 |
菜单服务的当前应用程序已更改 |
|
无 |
URL 哈希已更改 |
|
rpc ID |
一个 RPC 请求刚刚开始 |
|
rpc ID |
一个 RPC 请求已完成 |
|
无 |
Web 客户端已挂载 |
|
无 |
主视图应聚焦自身 |
|
无 |
所有内部缓存应被清除 |
|
函数列表 |
所有包含未提交更改的视图应清除这些更改,并将回调推送到列表中 |
浏览器对象¶
JavaScript 框架还提供了一个特殊的对象 browser
,它提供了对许多浏览器 API 的访问,例如 location
、localStorage
或 setTimeout
。例如,以下是如何使用 browser.setTimeout
函数的方法:
import { browser } from "@web/core/browser/browser";
// somewhere in code
browser.setTimeout(someFunction, 1000);
它主要用于测试目的:所有使用浏览器对象的代码都可以通过在测试期间模拟相关函数轻松进行测试。
它包含以下内容:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
调试模式¶
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 代码非常有用:一旦激活,assets 包将不再被压缩,并且还会生成源映射。这使得调试各种 JavaScript 代码变得更加方便。
测试模式¶
还有另一个名为 tests
的子模式:如果启用,服务器会在页面中注入 web.assets_tests
包。该包主要包含测试游览(其目的是测试功能,而不是向用户展示有趣的内容)。因此,tests
模式对于运行这些游览非常有用。
参见