第三章 Widget
widget是什么
Widget是WebClient旧世界里的可视化部件的基石。我们后面所讲到的视图,字段等都是它的子类。
Widget提供了多种用来管理局部DOM的方法。其功能主要有如下几点:
- 使用QWeb引擎来渲染页面。
- 生命周期管理(父对象被销毁时确保所有的子对象也一并销毁)。
- 将QWeb渲染的结果挂载到DOM中。
本章我们将先对Widget有个初步的认识,然后以一个的例子来带大家体会一下它的实际用途。
Hello, Widget
我们先来看一个最简单的Widget示例:
var MyWidget = Widget.extend({
templte: "MyQwebTemplate",
init: funtion(parent){
this._super(parent);
},
willStart: function(){
},
start: function(){
this.$(".my_button").click();
var promise = this._rpc(...);
return promise;
},
});
var myWidget = new Widget(this);
myWidget.appendTo($(".some-div"));
这是一个非常简单的Widget例子,tempate指明该widget要渲染的部件使用的模版名称。后边三个方法则描述了一个widget的生命周期的初期部分,包括初始化(init) , 将要启动(willStart) 和启动(start) 三个阶段。
初始化方法的作用是处理那些在渲染之前的初始化工作,willStart则用来处理那些在widget就绪(ready)之前需要同步的工作(例如通过rpc获取数据),它需要返回一个promise对象。start方法则是处理那些渲染之后需要完成的工作。
Widget属性
- tagName: 用来指定元素(div)
- Id: Widget的ID
- className: 样式名称
- attributes: 属性集合
- events: 事件集合
- template: QWeb的模板名称,缺省条件下要渲染的模版名称
- xmlDependencies: widget在渲染前要加载的xml文件路径列表
- cssLibs: widget在渲染前要加载的样式表文件路径列表
- jsLibs:widget在渲染前要加载的js文件路径列表
- assetLibs:widget在渲染前要加载的资源xmlid列表
Widget的生命周期
初始化(init)
初始化过程主要是初始化Mixin对象,设置父类对象,并自动绑定属性中以on_或者do_开头的函数。
init: function (parent) {
mixins.PropertiesMixin.init.call(this);
this.setParent(parent);
// Bind on_/do_* methods to this
// We might remove this automatic binding in the future
for (var name in this) {
if(typeof(this[name]) === "function") {
if((/^on_|^do_/).test(name)) {
this[name] = this[name].bind(this);
}
}
}
}
Mixin作用是用来构建对象的父子继承关系,每个对象都可以有一个父类对象和若干的子类对象。当对象被销毁,那些依赖它的子类对象也将一同被销毁并释放出所占用的资源。关于Mixin的更多内容,参见Mixin一章。这里我们只需要记住,Widget在初始化过程中,调用了Mixin的初始化方法(Widget也是Mixin的子类)。
即将启动(willStart)
willStart方法在init方法之后,start方法之前被调用,主要处理一些页面渲染必须的一些请求,然后调用start方法,willsStart方法返回一个promise对象,且promise对象被释放之后才可以执行start方法。
willStart: function () {
var proms = [];
if (this.xmlDependencies) {
proms.push.apply(proms, _.map(this.xmlDependencies, function (xmlPath) {
return ajax.loadXML(xmlPath, core.qweb);
}));
}
if (this.jsLibs || this.cssLibs || this.assetLibs) {
proms.push(ajax.loadLibs(this));
}
return Promise.all(proms);
}
正如我们最开始学习odoo开发时,要把xml定义在_mainfest_.py文件中的data节点中一样,xmlDependencies包含的就是Widget启动时需要加载的xml文件路径,其中不包含已经加载的文件。jsLibs、cssLibs和assetsLibs加载的是Widget启动的资源库文件。
启动(Start)
start方法在Widget被渲染之后启动,主要任务是绑定动作,触发异步调用等工作。依据惯例,start方法应该返回一个可以被promise.resolve()的对象,用来通知调用者,Widget已经初始化完成。需要注意的是,因为历史原因,很多widget的仍旧选择在start方法而非willStart方法中处理任务,虽然可能那些工作放在willStart中更为合适。
start: function () {
return Promise.resolve();
}
销毁(destroy)
Widget销毁的同时会一并销毁子Widget。
destroy: function () {
mixins.PropertiesMixin.destroy.call(this);
if (this.$el) {
this.$el.remove();
}
}
公开方法
虽然我们知道了Widget的生命周期包含willStart和Start的方法,但是willStart并非在init方法中启动的,而是在公开方法中被调用,然后启动了start方法。通常这些公开方法是在视图(view)中使用的。
appendTo
渲染当前的Widget并将Widget插入到给出的jQuery对象之后。此方法接受一个参数target,给定的jQuery目标对象。
appendTo: function (target) {
var self = this;
return this._widgetRenderAndInsert(function (t) {
self.$el.appendTo(t);
}, target);
}
prependTo
prependTo: function (target) {
var self = this;
return this._widgetRenderAndInsert(function (t) {
self.$el.prependTo(t);
}, target);
}
同appendTo相反,渲染部件并将部件添加到指定的元素之前。
insertAfter
渲染部件并将部件插入到指定的元素之后。
insertAfter: function (target) {
var self = this;
return this._widgetRenderAndInsert(function (t) {
self.$el.insertAfter(t);
}, target);
}
insertBefore
渲染部件并将部件插入到指定的元素之前。
insertBefore: function (target) {
var self = this;
return this._widgetRenderAndInsert(function (t) {
self.$el.insertBefore(t);
}, target);
}
replace
渲染Widget并替代给出的jQuery对象target。
replace: function (target) {
return this._widgetRenderAndInsert(_.bind(function (t) {
this.$el.replaceAll(t);
}, this), target);
}
上面5个方法的核心,都是在方法内部调用了私有方法_widgetRenderAndInsert,_widgetRenderAndInsert才是真正调用willStart并启动start的方法。
attachTo
将给出的元素附加到DOM文档中,接受一个参数target,jQuery目标对象。
attachTo: function (target) {
var self = this;
this.setElement(target.$el || target);
return this.willStart().then(function () {
return self.start();
});
}
attachTo方法也实现了先启动willStart再调用start的逻辑。
do_hide
隐藏Widget
do_hide: function () {
this.$el.addClass('o_hidden');
}
do_show
显示部件
do_show: function () {
this.$el.removeClass('o_hidden');
}
do_toggle
隐藏或显示部件
do_toggle: function (display) {
if (_.isBoolean(display)) {
display ? this.do_show() : this.do_hide();
} else {
this.$el.hasClass('o_hidden') ? this.do_show() : this.do_hide();
}
}
从上面三个方法的内容可以看出,隐藏和显示的方法是通过给元素添加或删除o_hidden样式实现的。
renderElement
渲染元素。默认使用QWeb框架渲染,指定的template必须是已经定义的模板,此方法会将部件以widget的关键字传入到QWeb中。因此,在template中,用户可以使用widget来引用必要的属性或方法。
renderElement: function () {
var $el;
if (this.template) {
$el = $(core.qweb.render(this.template, {widget: this}).trim());
} else {
$el = this._makeDescriptive();
}
this._replaceElement($el);
}
举例,在我们开发的14.0版本的销售历史价格模块中,需要自定义按钮的图标,我们不能将图标固定在QWeb中,需要动态指定图标样式,因此,可以使用widget变量来获取activity_exception_icon所指定的图标:
<div t-name="list_preview_widget.Preview">
<a tabindex="0" t-attf-class="fa text-primary"/>
</div>
setElement
参数:element
使用给定的元素重新设置widget的根元素。此方法会重新委托事件处理,重新绑定子元素,如果存在根元素,会将之前的元素一并替换。
setElement: function (element) {
if (this.$el) {
this._undelegateEvents();
}
this.$el = (element instanceof $) ? element : $(element);
this.el = this.$el[0];
this._delegateEvents();
return this;
}
私有方法
$
如果指定了选择器,则在当前部件的元素内查找符合选择器的元素,否则返回当前部件的元素。
$: function (selector) {
if (selector === undefined) {
return this.$el;
}
return this.$el.find(selector);
}
_delegateEvents
附加Widget的events属性中指定的事件处理函数。
_delegateEvents: function () {
var events = this.events;
if (_.isEmpty(events)) { return; }
for(var key in events) {
if (!events.hasOwnProperty(key)) { continue; }
var method = this.proxy(events[key]);
var match = /^(\S+)(\s+(.*))?$/.exec(key);
var event = match[1];
var selector = match[3];
event += '.widget_events';
if (!selector) {
this.$el.on(event, method);
} else {
this.$el.on(event, selector, method);
}
}
}
细心的同学可能会发现,Odoo官方的模块中,不仅出现了event而且还有另外一种custom_events,而custom_events并没有在Widget的源代码中定义。那么custom_events是何方神圣呢?
custom_events的定义不在Widget中,而是在Widget的父类对象Mixin中,关于Mixin的更多内容参见相关章节,这里只要知道custom_events也同样能实现事件代理的作用就OK了。
_makeDescriptive
根据Widget的声明构建一个潜在的根元素。
_makeDescriptive: function () {
var attrs = _.extend({}, this.attributes || {});
if (this.id) {
attrs.id = this.id;
}
if (this.className) {
attrs['class'] = this.className;
}
var $el = $(document.createElement(this.tagName));
if (!_.isEmpty(attrs)) {
$el.attr(attrs);
}
return $el;
}
_replaceElement
重置Widget的根元素,并用DOM中的新元素取代旧的根元素
_replaceElement: function ($el) {
var $oldel = this.$el;
this.setElement($el);
if ($oldel && !$oldel.is(this.$el)) {
if ($oldel.length > 1) {
$oldel.wrapAll('<div/>');
$oldel.parent().replaceWith(this.$el);
} else {
$oldel.replaceWith(this.$el);
}
}
return this;
}
_undelegateEvents
移除Widget所有的事件委托。
_undelegateEvents: function () {
this.$el.off('.widget_events');
}
_widgetRenderAndInsert
渲染Widget的低阶方法,这是一个私有方法,除了widget本身不应该被任何对象调用。
_widgetRenderAndInsert: function (insertion, target) {
var self = this;
return this.willStart().then(function () {
if (self.__parentedDestroyed) {
return;
}
self.renderElement();
insertion(target);
return self.start();
});
}
我们从代码中可以看出,不论是append、attach、insert、prepend还是replace,其内部都使用_widgetRenderAndInsert方法启动widget。因此,结合前面提到的生命周期,我们现在可以完整地画出widget启动的整个流程了:
实战
接下来我将带大家从头编写一个widget,我们把改widget命名为myWidget的widget。简单起见, 我们widget这里并不在视图上做任何更改, 仅仅在console里输出一些日志.
根据我们之前的学习, 我们首先要定义要widget继承自Widget:
var myWidget = Widget.extend({
start: function(){
console.log("my widget starting..")
},
destroy: function(){
console.log("oh, my widget is dying...")
},
on_attach_callback: function(){
console.log("hey, I'm attached.")
}
});
我们在start方法和destory方法中分别输出了一些日志. on_attach_callback方法是在widget嵌入到DOM中时调用的方法.
然后, 我们将我们的widget注册到Widget注册表中.
widgetRegistry.add("my_widget",myWidget);
return widgetRegistry;
最后,我们修改视图文件,将widget加入到表单视图中:
<group>
<widget name="my_widget"/>
<field name="authors" widget="many2many_tags"/>
<field name="date"/>
<field name="price"/>
<field name="img"/>
</group>
因为我们的widget并未对DOM做出任何操作,因为页面上不会发生变化,但是我们可以从控制台中看到我们的myWidget的一个生命周期:
总结
本章我们了解了Widget的基本构成和它的生命周期,而且了解了它的公开方法和私有方法。我们可以得到这样一个结论,Widget初始化之后,会被添加到DOM中,从而触发willStart方法和start方法,添加到DOM中的方式有两种,一种是通过attachTo方法,另外一种是通过调用了低阶方法_widgetRenderAndInsert的另外4种DOM操作方法。我们还知道了,给Widget添加事件的方式也有两种,一种是通过Widget本身定义的events属性,另外一种是通过custom_events属性。
Widget是Odoo前端构建的核心部件之一,之后我们会继续学习抽象字段,抽象字段是在Widget基础上构建的所有字段部件的基类,它是Widget的一个典型的拓展应用。继续加油吧!