发布在:jQuery UI > 小组件工厂

如何使用小组件工厂

虽然大多数现有的 jQuery 插件都是无状态的,即我们对元素调用它们,而这正是我们与插件交互的全部内容,但有一大类功能不适合基本的插件模式。

为了填补这一空白,jQuery UI 实施了更高级的插件系统。新系统管理状态,允许通过单个插件公开多个函数,并提供各种扩展点。此系统称为小组件工厂,作为 jQuery UI 1.8 的一部分以 jQuery.widget 的形式公开;但是,它可以独立于 jQuery UI 使用。

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

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

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

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

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 插件。在构建自己的插件时,您应该创建自己的命名空间。这可以清楚地表明插件的来源以及它是否属于更大的集合。

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

向小部件传递选项

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

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

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

为小部件设置默认选项

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 向小部件添加方法

现在我们已经可以初始化我们的进度条,我们将添加通过调用我们插件实例上的方法来执行操作的功能。要定义插件方法,我们只需在传递给 jQuery.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,以便您可以在插件中实现适当的功能。

绑定到小部件事件

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 深入了解

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

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

例如,jQuery UI 对话框小部件使用键"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 );
};

有关扩展小部件的更多信息,包括如何在现有小部件之上构建全新小部件,请参阅使用小部件工厂扩展小部件

link 清理

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

向小部件添加销毁方法

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

链接 结束语

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