第三章 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的一个典型的拓展应用。继续加油吧!

results matching ""

    No results matching ""