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

原文地址:http://blog.nemikor.com/2010/05/15/building-stateful-jquery-plugins/

一空白,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 对象,插件的参数就是像这样传递进来的。

本例中使用了 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 );
    }
});

 

给 widget 添加方法

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

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"));

 

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

Widget 的参数处理

有一个方法 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);
Widget Factory 背后的机制
jQuery.widget 被调用时,它会为你的插件创建一个构造函数, 并以插件定义体作为其 prototype。所有默认提供的方法来自于一个基础的 widget prototype, 定义在 jQuery.Widget.prototype。当插件实例化时, 它会被用 jQuery.data 的方式保存在原来的 DOM 元素里, 以插件名作为 key 值。
因为插件实例时直接绑定到 DOM 元素,你甚至可以直接访问到插件实例而不用 绕经那些暴露出来的插件方法。这样你就可以不用传方法名的方式而是直接去调用实例方法, 实例的属性也可以直接访问了。
var bar = $("<div></div>")
    .appendTo("body")
    .progressbar()
    .data("progressbar" );

// call a method directly on the plugin instance
bar.option("value", 50);

// access properties on the plugin instance
alert(bar.options.value);
使用构造函数和 prototype 的方式来实现插件的一个最大的好处, 就是使扩展插件变得很简单。通过修改插件的 prototype, 可以轻松的修改所有实例的行为。比如,你要添加一个方法用于重置进度为 0%, 只要给 prototype 添加这个方法,那么所有实例上都拥有这个功能了。
$.nmk.progressbar.prototype.reset = function() {
    this._setOption("value", 0);
};

清扫处理

有时候,插件让用户可以应用,然后过一阵再解除应用是有意义的。 这可以通过 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 和其它有状态的插件。

posted @ 2012-10-22 16:45  月月光  阅读(219)  评论(0编辑  收藏  举报