Widget 指南
id属性在程序或模块中应尽量避免使用,id限制了组件的可重用性,并往往使代码更脆弱.
如果确实需要,(如第三方类需要),id的设置如下:
this.id = _.uniqueId('my-widget-');
避免使用像“content” 或 “navigation”的类名,因为大家都有可能这样用,最好的办法是前面加个前缀,如jzj-xxxx
应避免使用全局选择器。因为一个组件可能在一个页面中使用多次(Odoo中的一个示例是仪表板),查询应限制在给定组件的范围内,
未过滤的选择,如 (选择器)或文档。 q u e r y S e l e c t o r A l l (选择器)通常会导致意外或错误的行为 . O d o o W e b ’ s W i d g e t ( ) 有一个属性, D O M 根节点 ( (选择器)或文档。querySelectorAll(选择器)通常会导致意外或错误的行为. Odoo Web’s Widget()有一个属性, DOM 根节点 ( (选择器)或文档。querySelectorAll(选择器)通常会导致意外或错误的行为.OdooWeb’sWidget()有一个属性,DOM根节点(el), 还有一个快速节点选择器 ( ( ) ) . 不要假设您的组件拥有或控制超出 ()). 不要假设您的组件拥有或控制超出 ()).不要假设您的组件拥有或控制超出el的任何东西(因此,避免使用对父窗口widget的引用)
Html模板/呈现应该使用QWeb.
所有交互组件(向屏幕显示信息或拦截DOM事件的组件)必须继承自Widget(),并正确实现和使用其API和生命周期
确保在使用$el之前等待start完成,例如:
var Widget = require('web.Widget');
var AlmostCorrectWidget = Widget.extend({
start: function () {
this.$el.hasClass(....) // in theory, $el is already set, but you don't know what the parent will do with it, better call super first
return this._super.apply(arguments);
},
});
var IncorrectWidget = Widget.extend({
start: function () {
this._super.apply(arguments); // the parent promise is lost, nobody will wait for the start of this widget
this.$el.hasClass(....)
},
});
var CorrectWidget = Widget.extend({
start: function () {
var self = this;
return this._super.apply(arguments).then(function() {
self.$el.hasClass(....) // this works, no promise is lost and the code executes in a controlled order: first super, then our code.
});
},
});
QWeb Template 引擎
web client使用QWeb Template 引擎渲染widget(),除非它们覆盖渲染方法以执行某些操作。
Qweb JS模板引擎基于XML,与python实现基本兼容。
那么,templates是怎样被加载的呢?. 每当web client启动时, 一个rpc被指向 /web/webclient/qweb 路由.
服务将返回一个模块中定义的所有模板列表(来自于manifest中),
web client 将等待加载模板列表,然后再启动其第一个widget。
这种机制对我们的需求非常有效,但有时,我们希望延迟加载模板,想象一下,我们有一个很少使用的widget,也许我们不希望在主文件中加载其模板,目的是使web客户端稍微轻一点。在这种情况下,我们可以使用widget的xmlDependencies键:
var Widget = require('web.Widget');
var Counter = Widget.extend({
template: 'some.template',
xmlDependencies: ['/myaddon/path/to/my/file.xml'],
...
});
这样,计数器widget将在其willStart方法中加载xmlDependencies文件,因此在执行渲染时template才加载。
Event System
目前,Odoo支持两个事件系统:一是一个简单的系统,允许添加侦听器和触发事件;另一个更完整的系统,也可以使事件“冒泡”。
这两个事件系统都在文件mixins.js中的EventDispatcherMixin中实现。这个mixin包含在Widget类中。
基本的事件系统:
这个事件系统是独创的,它实现了一个简单的总线模式. 有4个主要方法:
on: 在事件上注册侦听器
off: 移除事件上的侦听器
once: 这用于注册只调用一次的侦听器.
trigger: 触发事件。这将导致调用每个侦听器
示例:
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
start: function () {
this.counter = new Counter(this);
this.counter.on('valuechange', this, this._onValueChange);
var def = this.counter.appendTo(this.$el);
return Promise.all([def, this._super.apply(this, arguments)]);
},
_onValueChange: function (val) {
// do something with val
},
});
// in Counter widget, we need to call the trigger method:
... this.trigger('valuechange', someValue);
注意:不鼓励使用此事件系统,我们计划用扩展事件系统中的trigger_up方法替换每个触发器方法
Extended Event System
自定义事件widget是一个更高级的系统,它模拟DOM事件API。 无论何时一个事件被触发,它将构建组件树,直到构建后或被终止执行。
trigger_up: 这种方法将创建一个小的OdooEvent并在组件树中调度它。注意,它将从触发事件的组件开始
custom_events: 这相当于事件字典,但适用于odoo事件。
OdooEvent 类很简单, 它有三个公共属性, target (widget触发事件), name (事件名称) 和data (有效载荷). 同时它还有两个方法: stopPropagation 和 is_stopped.
可以更新前面的示例以使用自定义事件系统:
var Widget = require('web.Widget');
var Counter = require('myModule.Counter');
var MyWidget = Widget.extend({
custom_events: {
valuechange: '_onValueChange'
},
start: function () {
this.counter = new Counter(this);
var def = this.counter.appendTo(this.$el);
return Promise.all([def, this._super.apply(this, arguments)]);
},
_onValueChange: function(event) {
// do something with event.data.val
},
});
// in Counter widget, we need to call the trigger_up method:
... this.trigger_up('valuechange', {value: someValue});
注册
Odoo生态系统中的一个常见需求是从外部扩展/更改基本系统的行为(通过安装应用程序,即不同的模块),例如, 可能需要在某些视图中添加新的widget类型,基于这种情况, ODOO的做法是创建所需的组件,然后将其添加到注册表(注册步骤),以使web客户端的其余部分知道其存在.
注册分为以下几种情况:
字段注册 ( 从 web.field_registry exported)
例如:
var fieldRegistry = require('web.field_registry');
var FieldPad = ...;
fieldRegistry.add('pad', FieldPad);
它将是 AbstractField的子类。
view注册
此注册表包含 Web 客户端(尤其是视图管理器)已知的所有 JS 视图。此注册表的每个值都应该是AbstractView的子类。
action注册
值必须是AbstractAction。
widget之间的通信
组件之间有多种通信方式。
从parent到child
这是一个简单的案例。父widget可以简单地调用其子widget的方法:
this.someWidget.update(someInfo);
从一个widget到它的父级/某个祖先
在这种情况下,小部件的工作只是通知其环境发生了什么事。由于我们不希望小部件具有对其父级的引用(这会将小部件与其父级的实现耦合),因此最好的方法通常是触发一个事件,该事件将通过使用该trigger_up方法在组件树中冒泡:
this.trigger_up('open_record', { record: record, id: id});
此事件将在widget上触发,然后会冒泡并最终被某些上游widget捕获:
var SomeAncestor = Widget.extend({
custom_events: {
'open_record': '_onOpenRecord',
},
_onOpenRecord: function (event) {
var record = event.data.record;
var id = event.data.id;
// do something with the event.
},
});
跨组件
跨组件通信可以通过使用BUS总线来实现. 这不是首选的通信形式, 因为它的缺点是使代码更难维护.无论怎样, 它具有解耦组件的优点. 在下面这个场景中, 这只是通过触发和侦听BUS总线上的事件来实现相应功能。
// in WidgetA
var core = require('web.core');
var WidgetA = Widget.extend({
...
start: function () {
core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
},
});
// in WidgetB
var WidgetB = Widget.extend({
...
someFunction: function (barcode) {
core.bus.trigger('barcode_scanned', barcode);
},
});
在这个例子中,我们用到了web.core,但这不是必须的。