使用 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 ); } });
给 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"));
Note
初始化用所用的 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 和其它有状态的插件。