发布于:插件

高级插件概念

链接 提供对默认插件设置的公共访问

我们可以且应该对上述代码进行的一项改进是公开插件的默认设置。这很重要,因为这让插件用户能够以极少的代码轻松地覆盖/自定义插件。而这正是我们开始利用函数对象(function object)优势的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Plugin definition.
$.fn.hilight = function( options ) {
// Extend our default options with those provided.
// Note that the first argument to extend is an empty
// object – this is to keep from overriding our "defaults" object.
var opts = $.extend( {}, $.fn.hilight.defaults, options );
// Our plugin implementation code goes here.
};
// Plugin defaults – added as a property on our plugin function.
$.fn.hilight.defaults = {
foreground: "red",
background: "yellow"
};

现在用户可以在他们的脚本中包含这样一行代码

1
2
3
// This needs only be called once and does not
// have to be called from within a "ready" block
$.fn.hilight.defaults.foreground = "blue";

现在我们可以像这样调用插件方法,它将使用蓝色的前景色

1
$( "#myDiv" ).hilight();

如你所见,我们允许用户通过一行代码来更改插件的默认前景色。而且用户在需要时仍然可以有选择地覆盖这个新的默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Override plugin default foreground color.
$.fn.hilight.defaults.foreground = "blue";
// ...
// Invoke plugin using new defaults.
$( ".hilightDiv" ).hilight();
// ...
// Override default by passing options to plugin method.
$( "#green" ).hilight({
foreground: "green"
});

链接 在适用时提供对辅助函数的公共访问

这一项与前一项相辅相成,是扩展插件(并让其他人扩展你的插件)的一种有趣方式。例如,我们的插件实现可能会定义一个名为 "format" 的函数,用于格式化高亮文本。我们的插件现在可能看起来像这样,format 方法的默认实现在 hilight 函数下方定义

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
// Plugin definition.
$.fn.hilight = function( options ) {
// Iterate and reformat each matched element.
return this.each(function() {
var elem = $( this );
// ...
var markup = elem.html();
// Call our format function.
markup = $.fn.hilight.format( markup );
elem.html( markup );
});
};
// Define our format function.
$.fn.hilight.format = function( txt ) {
return "<strong>" + txt + "</strong>";
};

我们也可以轻而易举地在 options 对象上支持另一个属性,允许提供回调函数来覆盖默认格式化。这是支持插件自定义的另一种绝佳方式。这里展示的技术更进一步,通过实际公开 format 函数使其可以被重新定义。利用这种技术,其他人就可以发布针对你插件的自定义覆盖版本——换句话说,这意味着其他人可以为你的插件编写插件。

考虑到我们在本文中构建的这个简单示例插件,你可能会纳闷这到底什么时候会有用。一个真实的例子是 Cycle 插件。Cycle 插件是一个幻灯片插件,支持多种内置过渡效果——滚动、滑动、淡入淡出等。但实际上,无法定义出一个人可能想要应用到幻灯片过渡的每一种效果。这正是这种可扩展性发挥作用的地方。Cycle 插件公开了一个 "transitions" 对象,用户可以向其中添加自己的自定义过渡定义。它在插件中是这样定义的

1
2
3
4
5
$.fn.cycle.transitions = {
// ...
};

这种技术使得其他人能够定义并发布可以插入(plug-in)到 Cycle 插件中的过渡定义。

链接 保持私有函数私有化

公开插件的一部分以供覆盖的技术可能非常强大。但你需要仔细考虑公开实现的哪些部分。一旦公开,你需要记住,对调用参数或语义的任何更改都可能破坏向后兼容性。作为一般规则,如果你不确定是否要公开某个特定函数,那么你可能不应该这样做。

那么,我们如何在不污染命名空间且不公开实现的情况下定义更多函数呢?这是闭包(closures)的工作。为了演示,我们将向插件添加另一个名为 "debug" 的函数。debug 函数会将选定元素的数量记录到控制台。为了创建闭包,我们将整个插件定义包装在一个函数中(详见 jQuery 编写指南)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Create closure.
(function( $ ) {
// Plugin definition.
$.fn.hilight = function( options ) {
debug( this );
// ...
};
// Private function for debugging.
function debug( obj ) {
if ( window.console && window.console.log ) {
window.console.log( "hilight selection count: " + obj.length );
}
};
// ...
// End of closure.
})( jQuery );

我们的 "debug" 方法无法从闭包外部访问,因此它是我们实现的私有方法。

###鲍勃(Bob)和苏(Sue)

假设鲍勃创建了一个很棒的新图库插件(名为 "superGallery"),该插件接收图像列表并使它们可以导航。鲍勃加入了一些动画使其更有趣。他试图让插件尽可能地可自定义,最终写出了类似这样的代码

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
jQuery.fn.superGallery = function( options ) {
// Bob's default settings:
var defaults = {
textColor: "#000",
backgroundColor: "#fff",
fontSize: "1em",
delay: "quite long",
getTextFromTitle: true,
getTextFromRel: false,
getTextFromAlt: false,
animateWidth: true,
animateOpacity: true,
animateHeight: true,
animationDuration: 500,
clickImgToGoToNext: true,
clickImgToGoToLast: false,
nextButtonText: "next",
previousButtonText: "previous",
nextButtonTextColor: "red",
previousButtonTextColor: "red"
};
var settings = $.extend( {}, defaults, options );
return this.each(function() {
// Plugin code would go here...
});
};

你脑海中浮现的第一件事(好吧,也许不是第一件)可能是,为了适应这种程度的自定义,这个插件一定非常庞大。如果这个插件不是虚构的,它可能会比实际需要的要大得多。人们愿意花费的带宽(KB)是有限的!

现在,我们的朋友鲍勃认为这一切都很好;事实上,他对该插件及其自定义程度印象深刻。他认为所有的选项构成了一个更通用的解决方案,可以用于许多不同的情况。

我们的另一个朋友苏决定使用这个新插件。她设置了所有必需的选项,现在面前有一个可以运行的解决方案。仅在玩了五分钟插件后,她意识到如果每张图像的宽度能以较慢的速度制作动画,图库看起来会漂亮得多。她急忙查阅鲍勃的文档,但没有发现 animateWidthDuration 选项!

链接 你看到问题了吗?

这并不在于你的插件有多少选项,而在于它有什么选项!

鲍勃做得有点过头了。他提供的自定义级别虽然看起来很高,但实际上相当低,特别是考虑到在使用该插件时人们可能想要控制的所有可能事项。鲍勃犯了一个错误,即提供了大量极其具体的选项,这反而使他的插件更难以自定义!

链接 一个更好的模型

所以很明显:鲍勃需要一个新的自定义模型,一个不会放弃控制权或抽象掉必要细节的模型。

鲍勃之所以如此倾向于这种高层级的简单性,是因为 jQuery 框架本身非常契合这种思维方式。提供一个 previousButtonTextColor 选项固然简单美观,但让我们面对现实吧,绝大多数插件用户都会想要更多的控制权!

这里有一些技巧,应该能帮助你为插件创建一套更好的可自定义选项

链接 不要创建插件特有的语法

使用你插件的开发者不应该为了完成工作而去学习一套新的语言或术语。

鲍勃认为他通过 delay 选项(见上文)提供了最大的自定义程度。他设置成这样,使得通过他的插件你可以指定四种不同的延迟:“quite short”、“very short”、“quite long”或“very long”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var delayDuration = 0;
switch ( settings.delay ) {
case "very short":
delayDuration = 100;
break;
case "quite short":
delayDuration = 200;
break;
case "quite long":
delayDuration = 300;
break;
case "very long":
delayDuration = 400;
break;
default:
delayDuration = 200;
}

这不仅限制了人们拥有的控制级别,还占用了相当大的空间。仅仅为了定义延迟时间就用了十二行代码,有点过分了,你不觉得吗?构建此选项的一种更好方法是让插件用户将时间量(以毫秒为单位)指定为一个数字,这样就不需要对选项进行任何处理。

这里的关键是不要通过抽象来降低控制级别。你的抽象,无论是什么,都可以尽可能简单,但要确保使用你插件的人仍然拥有那种备受追捧的底层控制权!(这里的底层是指非抽象的。)

链接 给予对元素的完全控制

如果你的插件创建了要在 DOM 中使用的元素,那么最好为插件用户提供某种访问这些元素的方法。有时这意味着给某些元素指定 ID 或类。但请注意,你的插件内部不应该依赖这些钩子

一个糟糕的实现

1
2
3
4
// Plugin code
$( "<div class='gallery-wrapper' />" ).appendTo( "body" );
$( ".gallery-wrapper" ).append( "..." );

为了允许用户访问甚至操作这些信息,你可以将其存储在包含插件设置的变量中。下面显示了上述代码的一个更好的实现

1
2
3
4
5
6
7
// Retain an internal reference:
var wrapper = $( "<div />" )
.attr( settings.wrapperAttrs )
.appendTo( settings.container );
// Easy to reference later...
wrapper.append( "..." );

注意,我们创建了一个对注入的包装器的引用,并且我们还调用了 .attr() 方法来向元素添加任何指定的属性。因此,在我们的设置中,它可能会像这样处理

1
2
3
4
5
6
7
8
9
10
var defaults = {
wrapperAttrs : {
class: "gallery-wrapper"
},
// ... rest of settings ...
};
// We can use the extend method to merge options/settings as usual:
// But with the added first parameter of TRUE to signify a DEEP COPY:
var settings = $.extend( true, {}, defaults, options );

$.extend() 方法现在将递归遍历所有嵌套对象,为我们提供默认选项和传入选项的合并版本,并以传入选项为准。

插件用户现在有权指定该包装器元素的任何属性,因此如果他们需要任何 CSS 样式的钩子,他们可以非常轻松地添加一个类或更改 ID 的名称,而无需在插件源码中翻找。

同样的模型也可以用来让用户定义 CSS 样式

1
2
3
4
5
6
7
8
9
10
var defaults = {
wrapperCSS: {},
// ... rest of settings ...
};
// Later on in the plugin where we define the wrapper:
var wrapper = $( "<div />" )
.attr( settings.wrapperAttrs )
.css( settings.wrapperCSS ) // ** Set CSS!
.appendTo( settings.container );

你的插件可能有一个关联的样式表,开发者可以在其中添加 CSS 样式。即便是在这种情况下,提供一种在 JavaScript 中设置样式的简便方法也是个好主意,而无需使用选择器来获取元素。

链接 提供回调功能

什么是回调? —— 回调本质上是一个稍后被调用的函数,通常由某个事件触发。它作为参数传递,通常传递给组件的启动调用,在这种情况下,是一个 jQuery 插件。

如果你的插件是由事件驱动的,那么为每个事件提供回调功能可能是一个好主意。此外,你可以创建自己的自定义事件,然后为这些事件提供回调。在这个图库插件中,添加一个 "onImageShow" 回调可能是有意义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var defaults = {
// We define an empty anonymous function so that
// we don't need to check its existence before calling it.
onImageShow : function() {},
// ... rest of settings ...
};
// Later on in the plugin:
nextButton.on( "click", showNextImage );
function showNextImage() {
// Returns reference to the next image node
var image = getNextImage();
// Stuff to show the image here...
// Here's the callback:
settings.onImageShow.call( image );
}

与其通过传统方式(添加括号)启动回调,不如我们在 image 的上下文中调用它,image 将是对图像节点的引用。这意味着在回调中,你可以通过 this 关键字访问实际的图像节点

1
2
3
4
5
6
7
$( "ul.imgs li" ).superGallery({
onImageShow: function() {
$( this ).after( "<span>" + $( this ).attr( "longdesc" ) + "</span>" );
},
// ... other options ...
});

类似地,你可以添加 "onImageHide" 回调以及许多其他回调。回调的目的是让插件用户能够轻松地添加额外功能,而无需在源代码中翻找。

链接 记住,这是一种折中

你的插件不可能在每种情况下都工作。同样,如果你不提供或只提供极少的控制方法,它也不会非常有用。所以,请记住,这始终是一种折中。你必须始终考虑的三个因素是

  • 灵活性:你的插件能处理多少种情况?
  • 大小:你插件的大小是否与其功能级别相符?也就是说,如果一个非常基础的工具提示(tooltip)插件的大小是 20k,你会使用它吗?—— 可能不会!
  • 性能:你的插件是否会以任何方式大量处理选项?这是否会影响速度?所产生的开销对最终用户来说值得吗?