Javascript
Widgets
从web.Widget
输出class Widget(),是所有可视组件的基类,相当于mvc的view层,提供一系列的处理页面的方法
- 处理多widget之间的继承、被继承关系
- 提供可扩展的生命周期安全管理(当父类被destruct时自动将对应子类清除)
- 自动使用qweb引擎渲染
- 与backbone兼容的快捷方法
DOM根元素
Widget()负责的是根DOM下的一部分widget页面,可以通过两个属性来获取widget的DOM:
- Widget.el - widget对应的原始根DOM
- Widget.$el - 使用jQuery选择的el
有两种方法来定义生成DOM根元素:
- Widget.template - qweb的模板名,指定模板会在widget初始化之后、实际渲染之前渲染。该模板生成的根元素就会作为对应widget的DOM根元素
- Widget.tagName - 当没有设置模板名的时候使用,默认是
div
,它会被设置成widget的DOM根元素,可以通过以下属性来自定义对应DOM根元素: - Widget.id - 在DOM根元素上生成一个id属性
- Widget.className - 在DOM根元素上生成一个class属性
- Widget.attributes - 属性映射表,会自动将里面的键值对设置为根元素的对应属性
- 当设置了模板名,上述参数将不能使用
- Widget.renderElement() - 可以通过此方法来渲染widget的根DOM并设置,使用的是template或tagName,并调用setElement() 来设置
- 可以覆盖renderElement() 方法来实现自定义的渲染,但是如果没有在里面调用
__super
的话必须要调用setElement() 方法
使用widget
widget的生命周期分三个阶段:
- 1.创建并初始化
Widget.init(parent) - widget的初始化方法,可以接收更多的参数来覆盖父级widget
参数:
parent (Widget()) - 新创建的widget的父级,如果某widget没有父级可传null
- 2.注入DOM并启动,通过调用以下方法中的一个来完成
- Widget.appendTo(element) - 渲染widget并用jquery的appendTo添加到对应DOM最后一个后元素前
- Widget.prependTo(element) 渲染widget并用jquery的prependTo插入到对应DOM第一个子元素前
- Widget.insertAfter(element) - 渲染widget并用jquery的insertAfter添加到对应dom之后
- Widget.insertBefore(element) - 渲染widget并用jquery的insertBefore添加到对应dom之前
上述方法接收的参数和对应jquery方法接收的参数一致,会返回一个 deferred延迟执行对象,并赋予三个任务:
1.使用renderElement()来渲染widget的根元素
2.用对应的jquery方法将widget插入到dom中
3.启动widget并将启动的结果返回
Widget.start()
:当widget被插入到DOM之后异步启动,一般用于异步的rpc调用以获取远端数据用于widget中,完成后需要返回一个deferred对象。在start方法执行完成之前widget的功能不一定是完整的。
- 3.销毁并清除widget对象
Widget.destroy() - 销毁它的子类,解绑所有事件,将它的根元素从DOM移除。当父widget被销毁时会自动调用,如果它没有父类 或需要将当前widget移除但保留父级widget时 就必须显示调用
与widget销毁相关的函数:
-
Widget.alive(deferred[, reject=false])
由于RPC调用一般比较耗时,可能在它执行完成的时候widget已经被销毁了,这时在会在一个无效的widget对象上做操作,alive可用于处理rpc调用,并保证RPC调用返回后只在有效的widget上执行对应操作:
this.alive(this.model.query().all()).then(function (records) {
// would break if executed after the widget is destroyed, wrapping
// rpc in alive() prevents execution
_.each(records, function (record) {
self.$el.append(self.format(record));
});
});
参数:
deferred - deferred对象
reject - 默认情况下如rpc调用完成后widget已被销毁的话对应的deferred对象只是被封锁了,如果设置为True的话会将其调用拒绝
-
Widget.isDestroyed()
如果widget已经被销销毁了,会返回true,否则返回false
获取DOM内容
由于widget负责其DOM元素下的内容,可以用一个简便的方法去获取它DOM元素内的子片段:
Widget.$(selector)
将css选择器应用到widget的根DOM上
this.$(selector);
相当于this.$el.find(selector);
重置DOM根元素
Widget.setElement(element)
将widget的根DOM设置为指定的DOM,参数element需为一个DOM元素或相应的jquery对象
DOM事件处理
widget一般需要在相应页面内响应用户的动作,这需要通过将事件绑定到DOM元素上来实现。
- Widget.events
事件是一个事件选择器(事件名和css选择器之间以空格分开)- 回调函数的映射,回调函数可以是widget内置函数或一个函数对象,this表示相应widget
events: {
'click p.oe_some_class a': 'some_method',
'change input': function (e) {
e.stopPropagation();
}
},
回调函数只会被对应根DOM的匹配子元素触发。如果事件选择器留空的话,该事件是会被自动绑定到widget的根DOM上。
-
Widget.delegateEvents()
该方法用于将事件绑定到DOM,当设置好widget的根dom后会自动被调用,可以通过重写它来设置比events映射表指定的更为复杂的事件,但父级方法必须被显式调用,否则events不会被处理。 -
Widget.undelegateEvents()
用于在dom被销毁或重设时解绑events,当delegateEvents被覆盖时,它也需要进行覆盖。
该方法需要与backbone的delegateEvents相兼容
Widget子类
可以通过extend来创建Widget()的子类,并提供了一些抽象方法和具体方法用于使用。
var MyWidget = Widget.extend({
// 渲染对象时使用的qweb模板
template: "MyQWebTemplate",
events: {
// 事件绑定示例
'click .my-button': 'handle_click',
},
init: function(parent) {
this._super(parent);
// 在渲染之前执行的内容
// initialization
},
start: function() {
var sup = this._super();
// 渲染初始化逻辑
// 允许多重deferred对象
return $.when(
// 从父类获取异步信号
sup,
// 返回自己的异步信号
this.rpc(/* … */))
}
});
##使用
// 创建实例
var my_widget = new MyWidget(this);
// 渲染并插入到dom
my_widget.appendTo(".some-div");
##销毁
my_widget.destroy();
开发规范
- 避免使用id属性,用id会让部件重用变得很麻烦。可以使用class、dom节点或jquery来替代使用。如果一定要用的情况下,需要使用_.uniqueId()来特别声明:
this.id = _.uniqueId('my-widget-')
- 避免使用很通用的css名如content、navigation,以防止命名冲突。一般以根据它对应的部分来命名。
- 避免使用全局选择器,因为某个组件可能在同个页面重复使用如仪表板,像$(selector) or document.querySelectorAll(selector) 可能会导致错误的操作,使用widget对应的
($el)
或$()
来选择 - 不要认为你的部件拥有或控制它自己的
$el
- html模板和渲染需要合用qweb
- 所有用于显示信息或处理事件的交互性质的组件必须继承自Widget() 并且使用它的api正确的实现
RPC
为了进行显示和交互,需要使用rpc与odoo服务器通信,odoo提供两种api来处理:
- 底层基于JSON rpc与模块对应python片段通信
- 高级别的直接odoo模块调用
所有api都是异步调用的,所以它们都会返回deferred延期执行对象
高级别API 直接调用odoo模块
通过Model()
来访问odoo的对象方法,通过call方法(来自web.Model)和 query方法(来自web.DataModel)来访问odoo服务器对象
-
call()
是直接被映射到odoo服务端对象的同名方法的,用法跟odoo模型的api使用只有以下三点区别: - 交互是异步的,所以在rpc中是返回的deferred延期对象(它们会自己处理对应rpc调用结果)
- 由于javascript规范没有
__getattr__
和method_missing
特性,需要指定调度rpc的方法 - 没有池的概念,当需要用的时候就实例化model代理,而不是从另一个(如全局)中获取
var Users = new Model('res.users');
Users.call('change_password', ['oldpassword', 'newpassword'],
{context: some_context}).then(function (result) {
// do something with change_password result
});
-
query()
方法是搜索(odoo的search和read)的接口,返回一个Query()
对象,该对象不可改变但是可以基于它创建新的query对象,添加新的属性和方法到原始对象。
Users.query(['name', 'login', 'user_email', 'signature'])
.filter([['active', '=', true], ['company_id', '=', main_company]])
.limit(15)
.all().then(function (users) {
// do work with users records
});
query在调用all()
或first()
方法之前是不会实际执行的,这两个方法每次调用都会触发一个rpc请求。可以用来进行实时查询。
class Model(name)
- Model.name 对象所绑定的model名
- Model.call(method[, args][, kwargs]) 使用对应参数调用当前模型的对应方法
参数:
- method (String) 通过rpc调用的模型方法
- args (Array<>) 位置匹配的参数列表
- kwargs (Object<>) 传递的关键字参数
- Model.query(fields)
参数:fields (Array<String>) 搜索时需要获取的字段列表
class odoo.web.Query(fields)
第一部分方法是读取方法,它们使用所调用对象的数据来响应rpc请求
- odoo.web.Query.all() - 读取当前Query对象所对应的数据,返回一个deferred的数组
- odoo.web.Query.first() - 读取当前Query对象对应的第一个 结果,如果没有结果返回null,有结果返回deferred对象
- odoo.web.Query.count() - 获取当前Query对象可得到的记录数量
- odoo.web.Query.group_by(grouping...) - 获取查询数据的分组,
参数: grouping (Array<String>) - 分组列表
返回:Deferred<Array<odoo.web.QueryGroup>> | null
第二部分方法是设置方法,它们会创建一个新Query对象并对相关属性进行扩展或替换
- odoo.web.Query.context(ctx) 添指定的环境变量添加到搜索中
- odoo.web.Query.filter(domain) 将指定的domain表达式添加到查询条件中,通过and与已存在的domain联接
- odoo.web.Query.offset(offset) 设置查询的起始位置,会将原有的offset替换
- odoo.web.Query.limit(limit) 设置查询的数据量,会将原来的limit替换
- odoo.web.Query.order_by(fields…) 覆盖原来的排序规则,像Django的QuerySet.order_by一样
- 接收多个排序字段,按重要性高到低排序,第一个优先级最高,字段以字符串提供
- 每个字段默认是按升序,可以在字段前加
-
表示倒序
与django不同,它没有用?来进行随机乱序和对关联字段排序方法
**分组聚合 **
odoo有非常强大的分组运算功能,但是它是递归的,并且第N+1层依赖于第n层所提供的数据,所以当odoo.models.Model.read_group()工作时这个api就不是那么直观。
odoo一般用Query()方法来代替 read_group()方法。
some_query.group_by(['field1', 'field2']).then(function (groups) {
// do things with the fetched groups
});
该方法可以接受一个字段列表参数、也可以不带参数执行,无参数时直接返回null而不是deferred对象
当分组条件从其他地方来的时候,可以通过两种方法来测试:
- 对group_by所得到的结果进行检查:
var groups;
if (groups = some_query.group_by(gby)) {
groups.then(function (gs) {
// groups
});
}
// no groups
- 使用
when()
将返回值强制转换为deferred对象
$.when(some_query.group_by(gby)).then(function (groups) {
if (!groups) {
// No grouping
} else {
// grouping, even if there are no groups (groups
// itself could be an empty array)
}
});
成功的情况下group_by返回的结果是一个 QueryGroup()数组
class odoo.web.QueryGroup()
返回分组的属性key,有以下几种
- grouped_on -- 基于哪个字段进行分组计算
- value -- 当前分组的grouped_on的值
- length -- 分组内的记录数量
- aggregates -- 分组聚合结果的 {field: value} 映射
odoo.web.QueryGroup.query([fields...])
相当于Model.query() ,但只包含当前分组内的记录,返回一个Query对象供后续使用
odoo.web.QueryGroup.subgroups()
返回一个指向当前的子QueryGroup()的数组的deferred对象
底层API:RPC调用python程序
Session()
对象(通过web.Session实例化)的rpc方法提供了一个低级别api用来直接调用python程序,该方法接收一个完整URL、一个参数key=>value映射表 作为参数,并将对应获取的结果转换成json格式
session.rpc('/web/dataset/resequence', {
model: some_model,
ids: array_of_ids,
offset: 42
}).then(function (result) {
// resequence didn't error out
}, function () {
// an error occured during during call
});
web client
javascript模块系统
从odoo v8开始使用一套跟requirejs类似的js模块系统,它有以下优点:
- 依赖关系可以保证按顺序加载
- 更容易将文件分割成更小的逻辑单元
- 没有全局变量
- 很容易检查依赖关系,让重构变得容易很多
缺点:
- 如果要通过odoo交互就必须用模块系统加载,因为有很多对象只在模块系统中能用
- 不支持循环依赖
在这种模式下,通过require来导入需要的模块,并且显示声明所输出的对象。
odoo.define('addon_name.service', function (require) {
var utils = require('web.utils');
var Model = require('web.Model');
// do things with utils and Model
var something_useful = 15;
return {
something_useful: something_useful,
};
});
上面的代码创建了一个名叫addon_name.service的模块,使用odoo.define函数定义。
odoo.define函数有两个参数:
1.name - 新定义的模块名
2.function - 在里面定义该模块实际包含的内容,接收一个require参数,如果需要输出内容就需要有对应返回,require函数用于获取依赖的模块。
用javascript来导入需要的模块就声明输出内容,web客户端会自动进行加载。模块在文件中定义,一般最好一个文件对应一个模块。模块可以返回一个deferred对象,这样该模块只在deferred执行后才加载,而且模块可以被废弃,并在控制台记录对应信息:
Missing dependencies - 该模块不会出现在页面中,可能是javascript文件不在页面中或模块名有错误
Failed modules - 有javascript错误
Rejected modules - 模块返回的是一个废弃的deferred
Rejected linked modules - 该模块依赖于已废弃的模块
Non loaded modules - 模块所依赖的模块不存在或有错误
Web client结构
-
framework/
文件夹包含所有底层的模块 -
web.ajax
用于处理rpc调用 -
web.core
核心模块,给出很多有用的对象如qweb
,_t
-
web.Widget
包含widget类 -
web.Model
抽象化的web.ajax
,用于直接调用服务端模型的方法 -
web.session
如odoo.session
-
web.utils
有用的代码段 -
web.time
时间相关函数 -
views/
文件夹包含所有视图定义 -
widgets/
包含独立的部件 -
js/
文件夹包含一些重要的文件 -
action_manager.js
ActionManager 类 -
boot.js
模块系统的入口 -
menu.js
顶级菜单的定义 -
web_client.js
部件WebClient -
view_manager.js
包含ViewManager
还有其他两个文件:tour.js
用于tour,compatibility.js
用于将旧系统与新系统兼容,在这个文件中每个模块名被输出到全局变量odoo中。理论上模块可以不通过变量odoo使用。
javascript习惯
- 在模块最前面声明所有依赖关系,一般按模块名字母顺序来排列
- 在最后声明所有输出
- 在模块开始时添加
use strict
声明 - 以合适的名字命名模块如:
addon_name.description
- 类名首字母大写如:ActionManager ,但其他的小写如web.ajax
- 每个文件只定义一个模块
odoo web client测试
详见:http://www.odoo.com/documentation/10.0/reference/javascript.html#testing-in-odoo-web-client
译自odoo官方文档:http://www.odoo.com/documentation/10.0/reference/javascript.html ,不当之处欢迎批评指正。
内容发布自http://www.jianshu.com/u/6fdae8ec06bc,转载请注明出处
网友评论