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

Note

这一章节的内容是基于 Scott Gonzalez 一篇博客 Building Stateful jQuery Plugins(已获作者许可)

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

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

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

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

Example 8.3. 用 jQuery UI widget factory 创建一个简单的有状态的插件

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

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

Note

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

Example 8.4. 给 widget 传递参数

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

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

Example 8.5. 给 widget 设置默认值

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

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

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

Example 8.6. 创建 widget 的方法

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

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

    // create a public method
    value: function(value) {
        // no value passed, act as a getter
        if (value === undefined) {
            return this.options.value;
        // value passed, act as a setter
        } else {
            this.options.value = this._constrain(value);
            var progress = this.options.value + "%";
            this.element.text(progress);
        }
    },

    // create a private method
    _constrain: function(value) {
        if (value > 100) {
            value = 100;
        }
        if (value < 0) {
            value = 0;
        }
        return value;
    }
});

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

Example 8.7. 调用插件实例的方法

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

// get the current value
alert(bar.progressbar("value"));

// update the value
bar.progressbar("value", 50);

// get the current value again
alert(bar.progressbar("value"));

Note

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

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

Example 8.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() {
        var progress = this.options.value + "%";
        this.element.text(progress);
    }
});

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

Example 8.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() {
        var progress = this.options.value + "%";
        this.element.text(progress);
        if (this.options.value == 100) {
            this._trigger("complete", null, { value: 100 });
        }
    }
});

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

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

Example 8.10. 绑定 widget 事件

var bar = $("<div></div>")
    .appendTo("body")
    .progressbar({
        complete: function(event, data) {
            alert( "Callbacks are great!" );
        }
    })
    .bind("progressbarcomplete", function(event, data) {
        alert("Events bubble and support many handlers for extreme flexibility.");
        alert("The progress bar value is " + data.value);
    });

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

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

Example 8.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() {
        var progress = 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("");

        // call the base destroy function
        $.Widget.prototype.destroy.call(this);
    }
});

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

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

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

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

* 默认是单例

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

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

 

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

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

(function ($) {
    // utility functions (won’t be inherited)
    function foo() {}
    
    $.widget('命名空间.插件名', $.继承插件的命名空间.插件名,{ /* snip */ });
})(jQuery);        

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

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

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

public方法首字符不是_

private方法首字符是_


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

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

var instance = $('<div>');
instance.mywidget('publicFunction'); // work
instance.mywidget('_privateFunction'); // silently fail
instance.data('mywidget')._privateFunction(); // work
$.mynamespace.mywidget.prototype._privateFunction(); // work
 

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

image

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

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

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

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

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

_create  _init    destroy

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

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

var instance = $.data( this, name ); // 从widget自身取出名字为name的数据
if ( instance ) {
    instance.option( options || {} )._init();  // 若该数据已经存在则只调用_init
} else {
    $.data( this, name, new object( 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');

    // call the original destroy method since we overwrote it
    $.Widget.prototype.destroy.call(this);
}

 

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

var mw = $('#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('default option function1');
    }
},

调用:

$('#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=%s  value=%s', key, value);
    $.Widget.prototype._setOption.apply(this, arguments);
},

_setOptions: function (options) {
    var key;

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

    return this;
},

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

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

我们像下面这样调用:

var instance = $('<div>');

// create widget with default options
console.group();

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

console.groupEnd();

// create widget with specified options
instance.mywidget('destroy');
console.group();

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

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

console.groupEnd();

// modify options
console.group();

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

console.groupEnd();
 

打出的日志如下:

image

日志分为三大块。

第一块是不使用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是jQueryUI widget factory里的,和jQuery里$.fn命名空间下的trigger函数不是一个东西(后者不带下划线)。

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

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

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

// bind (overwrite, not add event handler)
mw.myWidget('option', 'myEvent', function (event, ui) {
    console.log('new implement');
});
// 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] )

callbackNameThe name of the event you want to dispatcheventObject(Optional)An (mocked) event object._trigger wraps this object and stores it in event.originalEventThe user receives an object with event.type ==this.widgetEventPrefix + "eventname"uiObject(Optional)An object containing useful properties the user may need to access.Protip: Use a method like._uito generate objects with a consistent schema. 

// 调用样例
this._trigger( "hover", e /* e.type == "mouseenter" */, { hovered: $(e.target)});
// The user can subscribe using an init option
$("#elem").filterable( { hover: function(e,ui) { } } );
// Or with traditional event binding/delegation
$("#elem").bind( "filterablehover" , function(e,ui) { } );
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值