发布于: jQuery UI > Widget Factory

如何使用小部件工厂

虽然大多数现有 jQuery 插件都是无状态的——也就是说,我们在一个元素上调用它们,这就是我们与插件交互的全部内容——但有一大类功能不符合基本的插件模式。

为了弥补这一空白,jQuery UI 实现了一个更高级的插件系统。新系统管理状态,允许通过单个插件暴露多个功能,并提供各种扩展点。该系统被称为 Widget 工厂,作为 jQuery UI 1.8 的一部分,它以 jQuery.widget 的形式暴露;但是,它可以独立于 jQuery UI 使用。

为了演示 Widget 工厂的功能,我们将构建一个简单的进度条插件。

首先,我们将创建一个进度条,它只允许我们设置一次进度。如下所示,这是通过调用 jQuery.widget() 并传入两个参数来完成的:要创建的插件名称,以及一个包含支持我们插件的函数的对象字面量。

当我们的插件被调用时,它将创建一个新的插件实例,所有函数都将在该实例的上下文中执行。这与标准 jQuery 插件在两个重要方面有所不同。首先,上下文是一个对象,而不是一个 DOM 元素。其次,上下文始终是单个对象,而不是集合。

使用 jQuery UI Widget 工厂的简单有状态插件

1
2
3
4
5
6
7
8
$.widget( "custom.progressbar", {
_create: function() {
var progress = this.options.value + "%";
this.element
.addClass( "progressbar" )
.text( progress );
}
});

插件的名称必须包含一个命名空间,在本例中我们使用了 custom 命名空间。您只能创建一级深的命名空间,因此 custom.progressbar 是一个有效的插件名称,而 very.custom.progressbar 则不是。

注意:在我们的示例中,我们使用 custom 命名空间。ui 命名空间保留用于官方 jQuery UI 插件。在构建自己的插件时,您应该创建自己的命名空间。这可以清楚地表明插件的来源以及它是否属于更大的集合。

我们还可以看到 widget 工厂为我们提供了两个属性。this.element 是一个只包含一个元素的 jQuery 对象。如果我们的插件是在包含多个元素的 jQuery 对象上调用的,那么将为每个元素创建一个单独的插件实例,并且每个实例都有自己的 this.element。第二个属性 this.options 是一个哈希表,包含我们插件所有选项的键/值对。这些选项可以按此所示传递给我们的插件。

向 widget 传递选项

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

当我们调用 jQuery.widget() 时,它通过向 jQuery.fn 添加一个方法来扩展 jQuery(与我们创建标准插件的方式相同)。它添加的函数名称基于您传递给 jQuery.widget() 的名称,不带命名空间——在我们的例子中,它将创建 jQuery.fn.progressbar。传递给我们插件的选项是设置在插件实例内部 this.options 中的值。

如下所示,我们可以为任何选项指定默认值。在设计 API 时,您应该找出插件最常见的用例,以便设置合适的默认值并使所有选项真正可选。

为 widget 设置默认选项

1
2
3
4
5
6
7
8
9
10
11
12
13
$.widget( "custom.progressbar", {
// Default options.
options: {
value: 0
},
_create: function() {
var progress = this.options.value + "%";
this.element
.addClass( "progressbar" )
.text( progress );
}
});

link 向 Widget 添加方法

现在我们可以初始化进度条了,我们将添加通过调用插件实例上的方法来执行操作的功能。要定义一个插件方法,我们只需将函数包含在我们传递给 jQuery.widget() 的对象字面量中。我们还可以通过在函数名称前加上下划线来定义“私有”方法。

创建 widget 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$.widget( "custom.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.
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;
}
});

要调用插件实例上的方法,您将方法名称传递给 jQuery 插件。如果您正在调用接受参数的方法,只需在方法名称之后传递这些参数即可。

注意:通过将方法名称传递给用于初始化插件的相同 jQuery 函数来执行方法可能看起来很奇怪。这样做是为了防止污染 jQuery 命名空间,同时保持方法调用链的能力。在本文后面,我们将看到可能感觉更自然的替代用法。

调用插件实例上的方法

1
2
3
4
5
6
7
8
9
10
11
12
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" ) );

link 使用选项

自动提供给我们插件的方法之一是 option() 方法。option() 方法允许您在初始化后获取和设置选项。此方法与 jQuery 的 .css().attr() 方法完全相同:您可以只传递一个名称将其用作 getter,传递一个名称和一个值将其用作单个 setter,或者传递一个名称/值对的哈希表来设置多个值。当用作 getter 时,插件将返回与传入名称对应的选项的当前值。当用作 setter 时,插件的 _setOption 方法将为正在设置的每个选项调用。我们可以在插件中指定 _setOption 方法来响应选项更改。对于独立于选项更改数量执行的操作,我们可以覆盖 _setOptions()

响应选项设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
this.options.value = this._constrain(this.options.value);
this.element.addClass( "progressbar" );
this.refresh();
},
_setOption: function( key, value ) {
if ( key === "value" ) {
value = this._constrain( value );
}
this._super( key, value );
},
_setOptions: function( options ) {
this._super( options );
this.refresh();
},
refresh: function() {
var progress = this.options.value + "%";
this.element.text( progress );
},
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
}
});

link 添加回调

使插件可扩展的最简单方法之一是添加回调,以便用户可以在插件状态更改时做出反应。我们可以在下面看到如何向我们的进度条添加回调,以表示进度何时达到 100%。_trigger() 方法接受三个参数:回调的名称、启动回调的 jQuery 事件对象以及与事件相关的哈希数据。回调名称是唯一必需的参数,但对于希望在您的插件之上实现自定义功能的用户来说,其他参数可能非常有用。例如,如果我们正在构建一个可拖动插件,我们可以在触发拖动回调时传递 mousemove 事件;这将允许用户根据事件对象提供的 x/y 坐标对拖动做出反应。

请注意,传递给 _trigger() 的事件必须是 jQuery 事件,而不是原生浏览器事件。

提供用户扩展的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
this.options.value = this._constrain(this.options.value);
this.element.addClass( "progressbar" );
this.refresh();
},
_setOption: function( key, value ) {
if ( key === "value" ) {
value = this._constrain( value );
}
this._super( key, value );
},
_setOptions: function( options ) {
this._super( options );
this.refresh();
},
refresh: function() {
var progress = this.options.value + "%";
this.element.text( progress );
if ( this.options.value == 100 ) {
this._trigger( "complete", null, { value: 100 } );
}
},
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
}
});

回调函数本质上只是附加选项,因此您可以像其他任何选项一样获取和设置它们。每当执行回调时,也会触发相应的事件。事件类型由插件名称和回调名称连接而成。回调和事件都接收相同的两个参数:一个事件对象和与事件相关的哈希数据,如下所示。

您的插件可能具有您希望允许用户阻止的功能。支持此功能的最佳方法是创建可取消的回调。用户可以通过调用 event.preventDefault() 或返回 false,以与取消任何事件相同的方式取消回调或其关联事件。如果用户取消回调,_trigger() 方法将返回 false,以便您可以在插件中实现相应的功能。

绑定到 widget 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
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 );

link 深入了解内部机制

现在我们已经了解了如何使用 widget 工厂构建插件,接下来让我们看看它是如何实际工作的。

当您调用 jQuery.widget() 时,它会为您的插件创建一个构造函数,并将您传入的对象字面量设置为您的插件实例的原型。自动添加到您插件的所有功能都来自一个基本 widget 原型,该原型定义为 jQuery.Widget.prototype。当创建插件实例时,它使用 jQuery.data 存储在原始 DOM 元素上,以插件的完整名称(插件的命名空间,加上一个连字符,再加上插件的名称)作为键。

例如,jQuery UI 对话框 widget 使用的键是 "ui-dialog"

由于插件实例直接链接到 DOM 元素,如果您愿意,可以直接访问插件实例,而无需通过暴露的插件方法。这将允许您直接在插件实例上调用方法,而不是将方法名称作为字符串传递,并且还将使您能够直接访问插件的属性。

1
2
3
4
5
6
7
8
9
10
var bar = $( "<div></div>" )
.appendTo( "body" )
.progressbar()
.data( "custom-progressbar" );
// Call a method directly on the plugin instance.
bar.option( "value", 50 );
// Access properties on the plugin instance.
alert( bar.options.value );

您也可以通过直接调用构造函数,传入选项和元素参数来创建实例,而无需通过插件方法。

1
2
3
4
var bar = $.custom.progressbar( {}, $( "<div></div>" ).appendTo( "body") );
// Same result as before.
alert( bar.options.value );

link 扩展插件的原型

为插件提供构造函数和原型最大的好处之一是易于扩展插件。通过添加或修改插件原型上的方法,我们可以修改插件所有实例的行为。例如,如果我们要为进度条添加一个方法以将进度重置为 0%,我们可以将此方法添加到原型中,它将立即可用于任何插件实例。

1
2
3
$.custom.progressbar.prototype.reset = function() {
this._setOption( "value", 0 );
};

有关扩展 widget 的更多信息,包括如何在现有 widget 的基础上构建全新的 widget,请参阅 使用 Widget 工厂扩展 Widgets

link 清理

在某些情况下,允许用户应用然后稍后取消应用您的插件是有意义的。您可以通过 _destroy() 方法来实现此目的。在 _destroy() 方法中,您应该撤消插件在初始化或后续使用期间可能执行的任何操作。_destroy()destroy() 方法调用,如果您的插件实例所关联的元素从 DOM 中移除,destroy() 方法会自动调用,因此这也可以用于垃圾回收。基本的 destroy() 方法还处理一些通用的清理操作,例如从 widget 的 DOM 元素中移除实例引用,解绑 widget 命名空间中所有元素上的事件,以及解绑使用 _bind() 添加的所有事件。

向 widget 添加销毁方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$.widget( "custom.progressbar", {
options: {
value: 0
},
_create: function() {
this.options.value = this._constrain(this.options.value);
this.element.addClass( "progressbar" );
this.refresh();
},
_setOption: function( key, value ) {
if ( key === "value" ) {
value = this._constrain( value );
}
this._super( key, value );
},
_setOptions: function( options ) {
this._super( options );
this.refresh();
},
refresh: function() {
var progress = this.options.value + "%";
this.element.text( progress );
if ( this.options.value == 100 ) {
this._trigger( "complete", null, { value: 100 } );
}
},
_constrain: function( value ) {
if ( value > 100 ) {
value = 100;
}
if ( value < 0 ) {
value = 0;
}
return value;
},
_destroy: function() {
this.element
.removeClass( "progressbar" )
.text( "" );
}
});

link 结束语

Widget 工厂只是创建有状态插件的一种方式。有几种不同的模型可以使用,每种模型都有其自身的优缺点。Widget 工厂为您解决了许多常见问题,可以大大提高生产力,它还大大提高了代码重用,使其非常适合 jQuery UI 以及许多其他有状态插件。