您的位置:首页 > Web前端 > JQuery

使用 jQuery UI Widget Factory 编写有状态的插件(Stateful Plugins)

2012-09-20 15:36 465 查看

下面只是其中的一单,如果需要查询整个内容可查看:

http://docs.huihoo.com/jquery/jquery-fundamentals/zh-cn/book.html

使用jQueryUIWidgetFactory编写有状态的插件(StatefulPlugins)

Note

这一章节的内容是基于ScottGonzalez一篇博客
BuildingStatefuljQueryPlugins(已获作者许可)

虽然大多数的jQuery插件都是无状态的(stateless),也就是说,与插件进行交互的就限于调用插件时的那一组对象,但是有好大一部分功能需求没办法通过这种简单的插件模式来实现。

为了填补这一空白,jQueryUI实现一套更加先进的插件系统。它可以管理状态,允许通过一个插件暴露多个函数,并提供多个扩展点。这套系统被称为widgetfactory,对应
jQuery.widget
,也是jQueryUI1.8的一部分。不过,它是可以独立于jQueryUI使用的。

我们接下来创建一个简单的进度条插件,用来演示widgetfactory的能力。

我们首先创建一个只能设置一次的进度条。下面是实现代码,使用
jQuery.widget
创建一个插件。它接受两个参数,插件名字和带有具体实现方法的对象。当插件被调用时,它会创建一个新的插件实例,而插件方法的执行对象也就是那个实例。这与标准jQuery插件实现有两点是很不一样的。一是,执行者是对象而不是DOM元素;二是,执行者永远是单个对象,而不是元素集。

Example8.3.用jQueryUIwidgetfactory创建一个简单的有状态的插件

$.widget("nmk.progressbar",{
_create:function(){
varprogress=this.options.value+"%";
this.element
.addClass("progressbar")
.text(progress);
}
});


插件名字必须包含一个命名空间,这里我们用了
nmk
这个命名空间。但这个命名空间有个限制——只允许一层,也就是说,我们不能使用像
nmk.foo
这样的命名空间。另外可以看到widgetfactory给我们提供了两个属性。一是
this.element
,它指向一个只包含一个元素的jQuery对象,如果插件是由包含多个元素的jQuery对象调用时,会给其中的每一个元素都分配一个插件实例,
并让
this.element
指向它;二是
this.options
,是包含键值对形式的插件参数的hash对象,插件的参数就是像这样传递进来的。

Note

本例中使用了
nmk
作为命名空间。命名空间
ui
则是保留给官方jQueryUI插件的。创建自己的插件的时候,应该使用自有的命名空间的,这样可以让人一看就清楚这插件哪来的,是否是一个大体系的一部分。

Example8.4.给widget传递参数

$("<div></div>")
.appendTo("body")
.progressbar({value:20});


当我们调用
jQuery.widget
时,与创建标准插件的方式一样,它也是通过往
jQuery.fn
上面添加方法的方式来扩展jQuery对象。而那个方法的名称就是我们定义的插件名称除去命名空间的部分,案例中是
jQuery.fn.progressbar
。调用时所传递的参数会传递给插件实例的
this.options
。在下面的代码中,我们可以在参数中设置一些默认值。在设计API的时候,你应该先搞清楚最通常的用例,并据此设定相应的默认参数,那么这些参数就成为可选项了。

Example8.5.给widget设置默认值

$.widget("nmk.progressbar",{
//defaultoptions
options:{
value:0
},

_create:function(){
varprogress=this.options.value+"%";
this.element
.addClass("progressbar")
.text(progress);
}
});


给widget添加方法

接下来就要初始化进度条了。我们使它可以通过调用插件实例方法的方式来执行一些操作。要给插件定义方法,只需要将其实现代码放在定义体内即可。我们也可以通过在方法名前加下划线的方式来定义“私有”方法。

Example8.6.创建widget的方法

$.widget("nmk.progressbar",{
options:{
value:0
},

_create:function(){
varprogress=this.options.value+"%";
this.element
.addClass("progressbar")
.text(progress);
},

//createapublicmethod
value:function(value){
//novaluepassed,actasagetter
if(value===undefined){
returnthis.options.value;
//valuepassed,actasasetter
}else{
this.options.value=this._constrain(value);
varprogress=this.options.value+"%";
this.element.text(progress);
}
},

//createaprivatemethod
_constrain:function(value){
if(value>100){
value=100;
}
if(value<0){
value=0;
}
returnvalue;
}
});


将方法名作为参数传进去即可调用插件实例的方法。如果调用的方法需要传递参数,只需要将那些参数作为后续参数一同传递。

Example8.7.调用插件实例的方法

varbar=$("<div></div>")
.appendTo("body")
.progressbar({value:20});

//getthecurrentvalue
alert(bar.progressbar("value"));

//updatethevalue
bar.progressbar("value",50);

//getthecurrentvalueagain
alert(bar.progressbar("value"));


Note

初始化用所用的jQuery方法,向它传递方法名就可以执行方法,这看起来似乎很奇怪。但这样可以在维持原来的链式调用的方式的同时,防止jQuery命名空间被污染。

Widget的参数处理

有一个方法
option
,是自动生成的。它可以实现在初始化过后,对参数进行查询或设置,就像css,attr的用法那样,只传名字时是查询,名字和值都有时是做设置,如果是包含键值对的hash对象则进行多项设置。进行查询时,插件会返回当前该参数的值。做设置时,插件的
_setOption
方法会被调用,修改多少个就调用多少次。我们可以自己实现
_setOption

方法来响应这些参数的修改。

Example8.8.当参数被修改时执行一些操作

$.widget("nmk.progressbar",{
options:{
value:0
},

_create:function(){
this.element.addClass("progressbar");
this._update();
},

_setOption:function(key,value){
this.options[key]=value;
this._update();
},

_update:function(){
varprogress=this.options.value+"%";
this.element.text(progress);
}
});


添加回调功能

扩展插件的一个最简单的办法就是添加回调功能,这样使用者就可以根据插件状态的改变来采取行动。下面,我们来尝试添加一个回调功能,在进度达到100%时触发。
_trigger
方法介绍三个参数:回调名称,触发回调的本地事件对象以及相关的数据。虽然其中只有回调名称是必须的,不过其它参数对使用者来说挺有用的。比如说,创建一个可拖拽插件,我们可以在触发回调时将原生的mouseover事件对象传递过去,用户在回调函数里就可以根据这个对象中的x/y
坐标对拖拽进行一些处理。

Example8.9.提供回调功能让用户进行扩展

$.widget("nmk.progressbar",{
options:{
value:0
},

_create:function(){
this.element.addClass("progressbar");
this._update();
},

_setOption:function(key,value){
this.options[key]=value;
this._update();
},

_update:function(){
varprogress=this.options.value+"%";
this.element.text(progress);
if(this.options.value==100){
this._trigger("complete",null,{value:100});
}
}
});


回调函数实际上只是另外一种参数,因此你也可以像其它参数一样进行查询和修改了。无论回调函数是否设置,事件都会触发的。事件类型则是由插件名称和回调名称合并而成。回调和事件被触发时会收到同样的两个参数:事件对象和相关数据。可以看下面的例子。

如果你的插件提供些功能是允许用户阻止操作的,最好的方式就是提供一个可撤销的回调。用户可以像撤销原生事件那样,调用
event.preventDefault()
或者
returnfalse
,去撤销回调和相关的事件。如果用户撤销了回调,
_trigger
方法会返回false,在插件中就可以据此采取相应的动作。

Example8.10.绑定widget事件

varbar=$("<div></div>")
.appendTo("body")
.progressbar({
complete:function(event,data){
alert("Callbacksaregreat!");
}
})
.bind("progressbarcomplete",function(event,data){
alert("Eventsbubbleandsupportmanyhandlersforextremeflexibility.");
alert("Theprogressbarvalueis"+data.value);
});

bar.progressbar("option","value",100);


WidgetFactory背后的机制
jQuery.widget
被调用时,它会为你的插件创建一个构造函数,并以插件定义体作为其prototype。所有默认提供的方法来自于一个基础的widgetprototype,定义在
jQuery.Widget.prototype
。当插件实例化时,它会被用
jQuery.data
的方式保存在原来的DOM元素里,以插件名作为key值。

因为插件实例时直接绑定到DOM元素,你甚至可以直接访问到插件实例而不用绕经那些暴露出来的插件方法。这样你就可以不用传方法名的方式而是直接去调用实例方法,实例的属性也可以直接访问了。

varbar=$("<div></div>")
.appendTo("body")
.progressbar()
.data("progressbar");

//callamethoddirectlyontheplugininstance
bar.option("value",50);

//accesspropertiesontheplugininstance
alert(bar.options.value);

使用构造函数和prototype的方式来实现插件的一个最大的好处,就是使扩展插件变得很简单。通过修改插件的prototype,可以轻松的修改所有实例的行为。比如,你要添加一个方法用于重置进度为0%,只要给prototype添加这个方法,那么所有实例上都拥有这个功能了。

$.nmk.progressbar.prototype.reset=function(){
this._setOption("value",0);
};


清扫处理

有时候,插件让用户可以应用,然后过一阵再解除应用是有意义的。这可以通过destroy方法的来实现。在
destroy
方法内部,你应该取消你的插件能造成的所有修改,初始化过程中或者后面的使用中造成的。
destroy
方法在DOM删除时会被自动调用,所以它可以用于垃圾回收。默认的
destroy
方法会删掉DOM元素与插件实例直接的连接,
所以在覆盖它时是调用原先插件提供的基础
destroy
方法,是很重要的。

Example8.11.给widget添加destroy方法

$.widget("nmk.progressbar",{
options:{
value:0
},

_create:function(){
this.element.addClass("progressbar");
this._update();
},

_setOption:function(key,value){
this.options[key]=value;
this._update();
},

_update:function(){
varprogress=this.options.value+"%";
this.element.text(progress);
if(this.options.value==100){
this._trigger("complete",null,{value:100});
}
},

destroy:function(){
this.element
.removeClass("progressbar")
.text("");

//callthebasedestroyfunction
$.Widget.prototype.destroy.call(this);
}
});


结论

Widgetfactory是创建有状态的插件的唯一途径。我们有不同的插件模型可供选用,各有优缺。Widgetfactory解决了大量基础性问题,有助于提高效率,有利于代码重用,非常适合用来创建jQueryUI和其它有状态的插件。

==================================================================================================================================

编写jQueryUI插件(widget)

使用jQueryUI的widget来写插件,相比于基本的jquery插件有一些好处:

*方便实现继承,代码重用

*默认是单例

*widget已经给你实现好的一些常用方法,例如destroy

带来好处的同时也带来了荆棘和陷阱,本文的目的就是梳理这些荆棘,标出哪里有陷阱。

基本知识:命名规范,public,private,this,this.element

如何开始写一个widget呢?模板如下:

(function($){
//utilityfunctions(won’tbeinherited)
functionfoo(){}

$.widget('命名空间.插件名',$.继承插件的命名空间.插件名,{/*snip*/});
})(jQuery);

其中命名空间是可选的,要不要继承别的widget也是可选的。大头是后面snip的部分,这也是下文要讲的。

一般来说工具函数写在widget外面比较合适,但如果你想要这些工具函数被子类继承,则需要写在widget里面。

写在widget里面的,就有public和private之分,规则是:

public方法首字符不是_

private方法首字符是_

当调用方法时,会先判断是否以_开头,如果是则不执行调用。

如果我非要在外面调用private方法,该怎么做?并非一点办法也没有:

varinstance=$('<div>');
instance.mywidget('publicFunction');//work
instance.mywidget('_privateFunction');//silentlyfail
instance.data('mywidget')._privateFunction();//work
$.mynamespace.mywidget.prototype._privateFunction();//work


在widget内,this表示的是什么?我们在widget的一个public函数内用console.log(this)打出来瞧瞧:





日志显示,this是一个$.widget.$.(anonymousfunction).(anonymousfunction)

this.element是变成widget的那个jQuery对象,如果要用jquery的方法,往往首先要取到jquery对象。

this.options是插件的选项,下文会详解。

this.__proto__包含了插件中定义的所有public和private函数,以及继承过来的方法。

这里简单介绍一下__proto__:每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个__proto__又会有自己的__proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念。

_create_initdestroy

widgetfactory实现了一种单例模式,即不允许在同一个jQuery对象上多次实例化。

当调用$(XX).widgetName()进行初始化的时候,会执行以下代码(源码截取自jquery.ui.widget.js):

varinstance=$.data(this,name);//从widget自身取出名字为name的数据
if(instance){
instance.option(options||{})._init();//若该数据已经存在则只调用_init
}else{
$.data(this,name,newobject(options,this));//若数据还没有则新建一个实例,并将实例保存
}


当调用$(XX).widgetName(‘destroy’)进行销毁的时候,执行以下代码(源码截取自jquery.ui.widget.js):

this.element
.unbind("."+this.widgetName)
.removeData(this.widgetName);//删除在create时保存的数据

有一个removeData的操作,那么下次调用$(XX).widgetName()就会重新实例化了。

需要注意的是,destroy方法在jquery.ui.widget.js中是有默认实现的,而_create和_init没有实现。因此如果用自己的方法覆盖destroy,不要忘记调用默认的:

destory:function(){
console.log('destory');

//calltheoriginaldestroymethodsinceweoverwroteit
$.Widget.prototype.destroy.call(this);
}


以下示例代码验证_create和_init的区别以及destroy的作用:

varmw=$('#test').myWidget();//_create_init
mw=$('#test').myWidget();//_init
mw.myWidget('destory');
mw=$('#test').myWidget();//_create_init


那么在_create和_init以及destroy里分别应该做什么:

_create:生成HTML,事件绑定。

_init:执行默认的初始化动作,例如把页面变成初始状态。

destory:调用$.Widget.prototype.destroy.call(this),删除HTML。

注意:绑定事件要注意给事件名加命名空间后缀:例如.bind('mouseenter.mywidget',this._hover)

options

选项,在widget中的定义是options,而在调用时是option,注意定义的时候有s,调用的时候没s。

定义:

option

s

:{
field1:'default',
function1:function(){
console.log('defaultoptionfunction1');
}
},


调用:

$('#test').mywidget('option','field1',2);


widget默认实现了两个函数:_setOptions和_setOption,_setOptions的实现就是对每个要修改的option调用_setOption,也就是说真正修改的动作在_setOption里。因此,如果要重写_setOption函数,则一定不要忘记写:

$.Widget.prototype._setOption.apply(this,arguments);


_setOptions和_setOption这俩函数什么时候被调用呢?用下面这个例子来说明。

例如有这样的_setOption和_setOptions:


_setOption:function(key,value){
console.log('_setOption:key=%svalue=%s',key,value);
$.Widget.prototype._setOption.apply(this,arguments);
},

_setOptions:function(options){
varkey;

console.group('_setOptions');
for(keyinoptions){
this._setOption(key,options[key]);
}
console.groupEnd();

returnthis;
},


以及一个打印options值的函数printOptions:

printOptions:function(){
console.group('options');
console.log('field1:%s',this.options.field1);
console.log('function1:%s',this.options.function1);
console.groupEnd();
},

我们像下面这样调用:

varinstance=$('<div>');

//createwidgetwithdefaultoptions
console.group();

instance.mywidget();
instance.mywidget('printOptions');

console.groupEnd();

//createwidgetwithspecifiedoptions
instance.mywidget('destroy');
console.group();

varopts={
field1:'specified',
function1:function(){
console.log('specifiedoptionfunction1');
},
};
instance.mywidget(opts);
instance.mywidget('printOptions');

console.log('-------------');
instance.mywidget(opts);

console.groupEnd();

//modifyoptions
console.group();

instance.mywidget('option','field1',2);
instance.mywidget('printOptions');

console.groupEnd();



打出的日志如下:





日志分为三大块。

第一块是不使用options来初始化,可以看到直接使用定义里默认的options,是不调用_setOption的。

第二块是使用options来初始化,这一块做了两个实验(日志中用--------将两块分隔),第一个实验是完全重建(_create,_init),从日志可以看到并没有调用_setOption;第二个实验只是重新初始化(_init),用的options都一样,从日志可以看到它调用了_setOption,且在_init之前调用的。

第三块不是初始化,而仅仅是修改option值,可以清楚看到调用了_setOption。

何时会调用_setOption的结论:

1.像instance.mywidget('option','field1',2);这样显式设置option时。

2.带着options初始化时:

如果实例不存在,即需要调用_create,则不调用_setOption;

如果实例已存在,仅需要调用_init,则会在调用_init之前调用_setOption。

_trigger

注意这个_trigger是jQueryUIwidgetfactory里的,和jQuery里$.fn命名空间下的trigger函数不是一个东西(后者不带下划线)。

_trigger一般用来回调用户传入options的callback。

在插件内部调用_trigger(‘myEvent’)即相当于调用options里面的myEvent这个回调函数。

要改动options里的eventhandler应该怎么做呢?不要使用bind/unbind,而是去修改options:

//bind(overwrite,notaddeventhandler)
mw.myWidget('option','myEvent',function(event,ui){
console.log('newimplement');
});
//unbind
mw.myWidget('option','myEvent',null);


总结一下:

this._trigger(‘eventName’)是widget特有的,用于调用options里定义的callback。

this.element.trigger(‘eventName’)是jQuery的,可参考jQuery的事件的用法。(其中this.element是表示该插件的jQuery对象)

一个_trigger的样例:

//模板

this._trigger("callbackName",[eventObject],[uiObject])

callbackName
Thenameoftheeventyouwanttodispatch
eventObject
(Optional)An(mocked)eventobject.
_trigger
wrapsthisobjectandstoresitin
event.originalEvent
Theuserreceivesanobjectwithevent.type==
this.widgetEventPrefix+"eventname"
uiObject
(Optional)Anobjectcontainingusefulpropertiestheusermayneedtoaccess.Protip:
Useamethodlike
._ui
togenerateobjectswithaconsistentschema.

//调用样例
this._trigger("hover",e/*e.type=="mouseenter"*/,{hovered:$(e.target)});
//Theusercansubscribeusinganinitoption
$("#elem").filterable({hover:function(e,ui){}});
//Orwithtraditionaleventbinding/delegation
$("#elem").bind("filterablehover",function(e,ui){});
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: