第二章 概述
我们在第一章简单认识了一下OWL之后, 接下来我们就将深入学习OWL的技术了,本章先从框架整体带大家对整个框架有个大概的认知.
代码结构
前端的代码架构基本都在web模块中, static/src文件夹下包含了基础的架构代码.
- core/文件夹核心底层代码
- fields/所有的字段组件
- views/所有的视图组件
- search/所有的控制组件
- webclient/web前端, 导航,菜单,动作...
我们在导入既有代码的时候可以使用@web前缀简化导入代码, 需要注意的是@web后包含/static/src部分.
import { memoize } from "@web/core/utils/functions";
WebClient的架构
下面是一个WebClient的示例架构:
<t t-name="web.WebClient" owl="1">
<body class="o_web_client">
<NavBar/>
<ActionContainer/>
<MainComponentsContainer/>
</body>
</t>
我们可以看到WebClient由三个分别包装了导航栏, 动作容器和组件容器组成, 动作容器用来显示当前的控制器和组织每个动作的协同. 组件容器用来显示所有在主组件容器中注册过的组件。
环境
WebClient作为OWL的一个应用, 它可以定义自己的环境变量, 组件可以通过this.env来访问这些环境变量. 下面是odoo添加的可以共享的变量列表:
- qweb: 包含所有的模板
- bus: 总线, 用来响应特定的事件
- services: 所有已发布的服务
- debug: 非空则运行在debug模式
- _t: 翻译函数
- isSmall: 是否移动端
四大部件
通常一个WebClient组件由四种抽象组件组成:
- registries: 注册中心
- services: 服务中心
- components: 组件中心
- hooks: 钩子中心
注册中心
注册中心(Registries)实际上就是一个key和value的字典, 一旦某个对象在注册中心注册, 那么WC的其他组件就可以使用. 例如:
import { registry } from "./core/registry";
class MyFieldChar extends owl.Component {
// some code
}
registry.category("fields").add("my_field_char", MyFieldChar);
这册中心可以根据用途的不同而进行不同的分类,例如,我们把字段作为一个类别,字段这个分类下就专门存放字段相关的组件。服务分类下就专门存放服务类的组件,以方便我们进行管理。
服务中心
服务(Services)是一段提供了某种特性的持续运行的代码. 它可以由组件或其他服务(使用useService)引入, 它也可以声明自己的依赖, 这也就是说service是一种依赖注入的系统. 比如, notification提供了一种展示通知的服务途径, 而rpc则提供了向后台请求服务的途径.
下面是一个自定义服务的例子, 该服务每5秒显示一个通知:
import { registry } from "./core/registry";
const myService = {
dependencies: ["notification"],
start(env, { notification }) {
let counter = 1;
setInterval(() => {
notification.add(`Tick Tock ${counter++}`);
}, 5000);
}
};
registry.category("services").add("myService", myService);
实现的效果:
组件和钩子
组件和钩子是来自于OWL中的概念, odoo中的组件是标准的OWL组件, 且是WC的组成部件.
钩子则是用来解耦代码方法,这是通过组合/注入的方式给某些既定的代码添加新的功能属性.它可以被当作某种混合实现(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中很重要的一个概念, 它实现了在调用方法或着进行RPC请求时附加额外信息的途径, 以供系统中的其他部分正确地响应这些动作. 这些额外的信息可以在系统中不断地进行传递,这在某些场景下面非常有用. 例如,可以让后台知道rpc请求的来源是哪个页面, 或者激活/禁用某些组件的特性.
WC中的上下文有两种:user context和action context. 因此我们只要注意context关键字可能含义.(根据不同的场景,其含义不同)
User Context
用户上下文是一小段关于当前用户的信息, 它可以通过user服务获取
class MyComponent extends Component {
setup() {
const user = useService("user");
console.log(user.context);
}
}
它的结构如下:
- allowed_company_ids: 当前用户所在的公司数组
- lang: 语言
- tz: 时区
实际中, orm服务自动添加了用户的上下文, 这也就是为什么我们在大多数情况下都不用显示引用user服务的原因.
Action Context
ir.actions.widow_action和ir.actioins.client是支持使用context的. 它在xml中的定义是一个文本, 但是当action在WC中被加载时,它将被转换成可执行对象,以配置动作响应.
<field name="context">{'search_default_customer': 1}</field>
它可以在多种途径下使用,比如视图每次请求服务端的时候将加载动作的上下文, 再比如搜索动作时使用的默认过滤.
有时候我们在编程上手动触发动作,拓展它的上下文是非常有用的:
// in setup
let actionService = useService("action");
// in some event handler
actionService.doAction("addon_name.something", {
additional_context:{
default_period_id: defaultPeriodId
}
});
Python解释器
因为我们在视图文件中的某型代码使用了python的语法, 因此我们在浏览器中进行渲染时,需要将这部分代码进行解释以保证浏览器能正常理解. 这就需要使用Python解释器了
import { evaluateExpr } from "@web/core/py_js/py";
evaluateExpr("1 + 2*{'a': 1}.get('b', 54) + v", { v: 33 }); // returns 142
py.js提供了5种对外的接口
- tokenize(expr): token化表达式
- parse(tokens): 解析抽象的语法树结构 参数是一个token
- parseExpr(expr): 解析抽象的语法树结构 参数是一个python式的表达式
- evaluate(ast[, context]): 依据上下文计算表达式的结果 参数式一个结构化的表达式
- evaluateExpr(expr[, context]): 依据上下文计算表达式的结果 参数式一个字符串python表达式
过滤
WC中的domain有两种形式,一种数组形式,一种字符形式
// 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)]"
字符形式的domain要比数组形式的强大, 它可以包含python表达式和未计算的值.
Domain在WC中如此重要,因此odoo提供了一个专门的类来处理:
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类
Domain类的定义
class Domain([descr])
...
其参数是一个字符串或数组或Domain
方法:
- contains(record): record若满足domain的过滤则返回true 否则false
- toString(): 返回一个字符串的domain表达式
- toList(): 返回一个数组的domain表达式
此外, Domain还有4个静态方法:
- Domain.and(domains): 获取domain的交集
- Domain.or(domains): 获取domain的并集
- Domain.not: 获取domain的非集
- Domain.combine(domains, operator): 获取domain的交集或并集,取决于operator
// ["&", ("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();
总线
WC的环境变量中有一个事件总线, 叫bus. 它的作用是将系统的各个组件间相互协作而避免代码耦合在一起. evn.bus是OWL中的eventbus的实现.
// for example, in some service code:
env.bus.on("WEB_CLIENT_READY", null, doSomething);
事件列表
名称 | 数据 | 触发器 |
---|---|---|
ACTION_MANAGER:UI-UPDATED | current,new,fullscreen | 当视图渲染事件完成时 |
ACTION_MANAGER:UPDATE | 下一个渲染信息 | 动作管理器完成开始计算下一个动作 |
MENUS:APP-CHANGED | none | 菜单切换 |
ROUTE_CHANGE | none | URL改变 |
RPC:REQUEST | rpc id | RPC请求开始 |
RPC:RESPONSE | rpc id | RPC请求结束 |
WEB_CLIENT_READY | none | Web Client挂载 |
FOCUS-VIEW | none | 主视图获取焦点 |
CLEAR-CACHES | none | 清空缓存数据 |
CLEAR-UNCOMMITTED-CHANGES | 方法列表 | 所有视图中未保存的变化被清空 |
浏览器对象
browser对象提供了对浏览器的API,包括:location, locationStorage或setTimeout等.
import { browser } from "@web/core/browser/browser";
// somewhere in code
browser.setTimeout(someFunction, 1000);
这主要是出于测试的目的:所有使用浏览器对象的代码都可以通过在测试过程中模拟相关的函数来轻松测试
Debug模式
Odoo有时可以在一种特殊的模式下运行,称为调试模式。它主要用于两个目的。
显示某些特定屏幕的额外信息/字段。
提供一些额外的工具来帮助开发者调试Odoo的界面。
debug模式是由一个字符串描述的。一个空的字符串意味着debug模式没有被激活。否则,它就是激活的。如果字符串包含assets或test,那么相应的特定子模式将被激活(见下文)。两种模式可以同时激活,例如,字符串assets,test