第十章 插件

 

像插头插进插座
我就在身边等你
——Devo
“难道你不知道吗”(认为翻译成“你知道吗”更好)


整本书中,我们已经研究过许多办法,让jQuery类库可以用来完成多种任务。另一个方面是其核心,其优雅的插件架构允许开发者对jQuery进行扩展,使它成为一个更多功能的类库。

虽然jQuery已面世不到两年时间,它已经支持了百余插件——从选择器助手到全面的用户界面构件。在这一章中我们会简要介绍三个热门jQuery插件,然后创建一些我们自己的。

我们已经讨论过插件的力量,并且在第七章已经做过一个简单的例子。在这,我们将要看看把已经存在的插件整合到我们的网页当中,以及更细致的研究如何建立自己的插件。


如何使用一个插件

使用一个jQuery插件是非常直接的。第一步是在文档的<head>标签中包含(或许这个词直接写include更好些)进来,并确保出现在jQuery主体之后。源文件:

<head>
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <script src="jquery.js" type="text/javascript"></script>
  <script src="jquery.plug-in.js" type="text/javascript"></script>
  <script src="custom.js" type="text/javascript"></script>
  <title>Example</title>
</head>

然后,要做的事情就是包含自定义的Javascript文件,其中是我们创建或扩展的要使用的方法。例如,使用表单插件,我们可以在自定义的文件里面增加一行$(document).ready()方法来让一个表单通过AJAX提交:

$(document).ready(function() {
  $('#myForm').ajaxForm();
});

许多插件都有些内建的灵活性,以及提供了许多可选参数,我们可以开始修改他们的行为。我们可以根据需要定制插件的选项,或者直接简单的使用默认值。


流行的插件

当前jQuery的网站提供了一个很长的可获得的插件列表,网址是http://jquery.com/Plugins,并计划在工程中增加新的功能,如用户评级和评论,帮助访问者确定哪些是最受欢迎的。

在这一章里,我们将研究三个官方的插件——之所以这样安排是因为他们成熟的代码基础,可用性,并遵守一套由jQuery项目制定的编码和文档的标准。

Dimensions
度量插件,由Paul Bakaus和Brandon Aaron合作编写的。开发者们需要计算一个文档中元素的高度和宽度,而这个插件正好帮助他们跨越了与CSS盒子模型之间的鸿沟。它同时也可以计算元素相对顶部和左边的坐标,不管它们出现在页面的什么地方。


高度和宽度

对于计算高度和宽度,Dimensions提供了三套方法:
  1. .height()和.width()
  2. .innerHeight()和.innerWidth()
  3. .outerHeight()和.outerWidth()
当.height和.widht方法应用到元素上时只是使用了jQuery同名的核心方法。而Dimensions扩展了这两个方法从而我们可以把他们应用到浏览器的window和document上。例如,使用$(windwo).width()将会返回浏览器宽度的象素,而$(document).width()将同样返回document单独的宽度。如果有垂直滚动条,$(window).width()会包括它而$(document).width()不会。

inner和outer方法对于计算元素包含padding(inner和outer)和border(outer)的宽度和高度非常有用。我们来看一个元素实例<div class="dim-outer">,他的CSS如下:
  .dim-outer{
    height: 200px;
    width: 200px;
    margin: 10px;
    padding: 1em;
    border: 5px solid #e3e3e3;
    overflow: auto;
    font-size: 12px;
    }
普通的$('div.dim-outer').width()方法返回200,因为事实上那是定义在CSS里的宽度。然而,如果我想要从左边框到右边框之间内部的宽度它就不是个那么精确了。因此,我们可以使用$(''div.dim-outer).innerWidth(),它将返回224。多出来的24象素来自左右的padding之和。既然padding是1em,每个em又等于font-size,我们设置的是12px,所以我们得到总数是24个多余的象素。对于$('div.dim-outer').outerWidth(),我们给元素的宽度(+200)增加了左右边框(5+5)和padding(+24)一共达到从外边缘到外边缘的234。

(图)


ScrollTop和ScrollLeft

.scrollTop和.scrollLeft方法分别返回浏览器中用户已经滚动的或一个文档中可滚动的一个元素的下和右坐标。当同时使用一个数字参数时,他们也可以把页面移动到给出的滚动位置。


Offset

或许Dimensions插件最强大的特性就在于它的.offset()方法了,图可以让我们定位在页面任何地方的任何元素的top和left,不管它的position是static,relative,或是absolute,并且也不管窗口的滚动条甚至是元素的滚动条是否将overflow属性设置成了auto。使用选项中的各个因子margin,border,padding和scroll纳入计算,.offset()提供了巨大的可适应性和精确性。Dimensions的测试页面可以给出一个它神通广大的感性认识:

(图)

在这,点击Move to inline 1这个链接就将灰色盒子准确的移动到以它的顶部和左边边框重叠在inline 1元素的位置,因为border选项设置的是false。要看更多的例子,请访问这个测试页面:http://brandon.jquery.com/plugins/dimensions/test/offset.html


Form
Form插件是个使一个很难很复杂的任务变得处理起来很简单的一个脚本例子,甚至能让人觉得恐怖。

这个插件的核心就是.ajaxForm方法。正如我们在“怎样使用插件”部分看到的,将一个传统的表单转换成AJAX表单只需要一行很简单的代码:
$(document).ready(function(){
  $('#myForm').ajaxForm();
});
这个例子将会把一个id="myForm"的表单转变成一个不需要刷新当前页面就可以提交的表单。这一特点本身是不错的,但真正的力量是来自我们传递给方法的选项表。例如,下面的代码调用.ajaxForm()方法时同时使用了target,beforeSubmit和success选项:
$(document).ready(function() {
  function validateForm() {
    // the form validation code would go here
    // we can return false to abort the submit
  };
  $('#test-form').ajaxForm({
    target: '.log',
    beforeSubmit: validateForm,
    success: function() {
      alert('Thanks for your comment!');
    }
  });
});
这里的target选项表明了那些元素——在这个例子中是所有class="log"的元素——会被来自服务器的响应更新。
beforeSubmit选项在表单被提交之前起作用。这里调用了validateForm函数。如果它返回false,那么表单将不会被提交。
success选项在表单成功提交后起作用。在这个例子中,它只是提供了一个警告消息来让用户知道表单已经被提交了。
其他可以在.ajaxForm()和与之相似的.ajaxSubmit()的选项包括:
· url:如果和表单的action属性不同的话,表单数据将提交到的URL。
· type:提交表单的使用的方法——GET或者POST其中之一。默认的是表单的method属性,如果没有提供的话就是GET。
· dataType:期待从服务器返回的数据类型。可能的值是null,xml,script或json。默认值是null。
· resetForm:Boolean类型;默认false。如果设置成true,当表单提交成功后所有的表单域的值都会被清空。
Form插件提供了大量的方法来帮助处理表单和他们的数据。如果想更进一步了解这些方法,并且查看演示和样例,请访问http://www.malsup.com/jquery/form/

技巧和窍门
.ajaxForm()和.ajaxSubmit()都默认使用表单标记里的action和method属性。当我们使用越来越适合的表单标记时,插件将正如我们所预期的工作而无须再加工。

通常当一个表单被提交后,被表单提交的元素曾有一个name,那么它的name/value就随着表单其余的数据被提交了。.ajaxForm方法主动的在这点上给所有的提交元素增加了点击处理器以至于它知道那一个提交了表单。另一方面,.ajaxSubmit()方法是被动的,没有方法终止(决定?)这些信息。它并没有捕获那些提交中的元素。在应用到图像input元素时有相同的差别:.ajaxForm()处理他们,而.ajaxSubmit()忽略他们。(这段没翻好)

.ajaxForm()和.ajaxSubmit()方法是将可选参数传递给jQuery的核心方法$.ajax()。因此,$.ajax()任何合法的选项都可以通过form插件传递。从这一点出发,我们甚至可以使AJAX表单的返回更有活力,比如:
$(#myForm).ajaxForm({
  timeout: 2000,
  error: function (xml, status, e) {
    alert(e.message);
  }
});
.ajaxFrom和.ajaxSubmit方法可以将一个函数作为可选参数传递。因为这个加工过的函数被当作返回成功处理器,我们可像这样得到从服务器返回的文本:
$(#myForm).ajaxForm(function(responseText) {
  alert(responseText);
});


Interface

Dimensions和Form插件能把一些事情做的很好,而Interface也可以做很多事情(并且也能做的很好)。事实上,Interface并不像一个插件,而更像一整套的插件。

最初由Stefan Petre创建,而Paul Bakaus做出了主要的贡献。Interface有助于使web的体验变得更像一个桌面应用程序,有特色的拖放和项目排序小组件,以及动画效果和丰富的视觉回应。

在这让一起我们简要的研究下Animate和Soutables插件。


Animate

正如Dimension插件的.height和.width方法,Interface中的.animate方法也是从jQuery的核心方法扩展而来的。在Interface中的.animate()开放了那些jQuery核心中相关参数选项的有限集合,来包含关于任何CSS的属性,甚至是一个class的名字。例如,Interface的.animate()可以以动画的形式把class的一组属性改变成另一组。如果我们有一个这样的元素<div class="boxbefore">和以下的CSS规则
(这段翻译的不好。。。)
.boxbefore {
  width: 300px;
  margin: 1em 0;
  padding: 5px;
  overflow: auto;
  background-color: #fff;
  color: #000;
  border: 10px solid #333;
}
这个样式给我们描述了这样一个盒子,300象素宽,每边5象素内边距,10象素宽深灰色的边框,以及一般的黑色文字和白色背景。overflow属性被设置成auto以使得当这个盒子没有足够大来显示所有内容的时候滚动条会出现。然而,由于没有指定高度,这个盒子会随着内容适应大小。根据这些属性,我们的盒子应该看起来是这样的:

(图)

现在来让我们用动画的把boxbefore class改变成一个新的boxafter class,包含以下的属性:
.boxafter {
  height: 180px;
  width: 500px;
  padding: 15px;
  background-color: #000;
  color: #fff;
  border: 5px solid #ccc;
}
根据CSS规则,我们正把盒子设置成高度180象素,扩大它的宽度到500象素,减少边框的宽度并让它的颜色变亮,增加内边距,并置换文字和背景的颜色。由于我们没有定义新的overflow和margin属性,他们会保持原样。

趋使这个简单的变化,我们只需要写下面的一行代码:
$(document).ready(function() {
  $('div.boxbefore').animate({className:'boxafter'}, 1000);
});
动画进行了略一半多时我们的盒子看起来是这个样子的:

(图)

当动画停止时,盒子已经应用了所有boxafter class的样式。同时出现了垂直滚动条因为overflow:auto;踢掉了(kicks in?)减少的高度:

(图)


Sortables
在Interface里Sortables插件模块可以把任意元素的组转化成一个拖放样式列表。在这,我们有一个无序的列表,里面的项目应用了一些CSS样式:

(图)

HTML是非常直白的:
<ul id="sort-container" class="content">
  <li id="item1" class="sort-item">John</li>
  <li id="item2" class="sort-item">Paul</li>
  <li id="item3" class="sort-item">George</li>
  <li id="item4" class="sort-item">Pete</li>
  <li id="item5" class="sort-item">Stu</li>
  <li id="item6" class="sort-item">Ringo</li>
</ul>
每个列表项都有一个唯一的id和一个普通的class。现在,为了让这个列表可排序,我们只需写以下的代码:
$(document).ready(function() {
  $('#sort-container').Sortable({
    accept : 'sort-item',
    hoverclass : 'hover',
    helperclass : 'helper',
    opacity:   0.5
  });
});
这段代码由一个单一的.Sortable方法和一个参数表组成。首先accept是一个必须的参数而其他的是可选的。实际上我们在脚本之外留出了不少选项。

正如我们所见,这个方法让任何一个有class="sort-item"的元素变得可排序(sortable?)。它也使每个元素都应用了一个当鼠标指针悬停在元素上时的class(hoverclass:'hover')并且识别出给helper元素用的class(helperclass:'helper')。在这个例子中,helper class只不过是一个红色的虚线边框:

(图)

像Sortables的Interface插件有助于对我们的web应用提供类似桌面应用程序的功能。想知道更多关于Interface插件的全部信息,请访问:http://interface.eyecon.ro/


查找插件文档

jquery.com的插件知识库在http://jquery.com/Plugins/,是一个查找文档很好的出发点。每个插件都被列在知识库里,并且都链接了可以下载这个插件的页面。加之很多链接的页面都包含演示,样例代码和指南,来帮助我们开始学习。

官方的jQuery插件同样也在源代码中提供了充足的注释。对于很多插件,他们注释的写法和jquery.js注释的写法相同,为每个方法都提供了描述和至少一个例子。这意味着这些工具可供查阅的jQuery文档也在插件中也适用。

例如,Dimension插件的.offset方法有这些注释:
/**
 * Returns the location of the element in pixels from the top left
 * corner of the viewport.
 * 返回从左上角算起的元素的位置。
 *
 * For accurate readings make sure to use pixel values for margins,
 * borders and padding.
 * 为了正确的阅读,请确保对于margin,border和padding使用象素单位。
 *
 * @example $("#testdiv").offset()
 * @result { top: 100, left: 100, scrollTop: 10, scrollLeft: 10 }
 *
 * @example $("#testdiv").offset({ scroll: false })
 * @result { top: 90, left: 90 }
 *
 * @example var offset = {}
 * $("#testdiv").offset({ scroll: false }, offset)
 * @result offset = { top: 90, left: 90 }
 *
 * @name offset
 * @param Object options A hash [map] of options describing what
 * should be included in the final calculations of the offset.
 * The options include:
 * @param 对象类型 选项 一个选项的哈希表,描述了哪些需要包含在最后计算
 * 出来的offset之中。
 * 这些选项包括:
 *      margin: Should the margin of the element be included in the
 *          calculations? True by default.
 *   元素的margin应该包含在计算中吗?默认是true。
 *          If set to false the margin of the element is subtracted 
 *                                            from the total offset.
 *   如果设置成了false那么margin会从元素的总offset中被减去。
 *      border: Should the border of the element be included in the
 *          calculations? True by default.
 *   同margin的设置
 *          If set to false the border of the element is subtracted
 *                                            from the total offset.
 *      padding: Should the padding of the element be included in the
 *          calculations? False by default.
 *   注意,默认是false。
 *          If set to true the padding of the element is added to the
 *          total offset.
 *      scroll: Should the scroll offsets of the parent elements be
 *          included in the calculations? True by default. When true,
 *          it adds the total scroll offsets of all parents to the
 *          total offset and also adds two properties to the returned
 *          object, scrollTop and scrollLeft. If set to false the
 *                     scroll offsets of parent elements are ignored.
 *          If scroll offsets are not needed, set to false to get a
 *          performance boost.
 *   默认是true。当设置成true的时候,它会在总的offset中增加
 *  所有父级的scroll offset同时也给返回的对象增加了两个属性,
 *  scrollTop和scrollLeft。如果设置成false,父级元素的scroll offset
 *  会被忽略。如果scroll offset不需要的话,设置成false来得到运行的
 *  改善。
 *  
 * @param Object returnObject An object to store the return value in,
 * so as not to break the chain. If passed in, the chain will not be
 * broken and the result will be assigned to this object.
 *
 * @type Object
 * @cat Plugins/Dimensions
 * @author Brandon Aaron (brandon.aaron@gmail.com ||
 *                                        http://brandonaaron.net)
 */

在这,我们可以看到这段注释由一段方法的大概注释和一些关于使用象素值的简要建议开始。接下来这段介绍性的文字是一个更多细节信息的列表,每一个列表项都以一个@符号作为起始。请注意方法的名字(@name offset)直到例子后面才出现。三个例子的顺序是随着复杂程度渐增来安排的。

方法名后面紧跟着的是可以使用的参数。这些参数,尤其是object选项,都描述的非常细致,没有默认值和什么我们可以预期(what we can expect)如果我们应用他们。

最后三个项目提供了关于这个方法的更多信息,包括返回数据的类型,它的范畴,和作者。

如果我们不能在插件知识库,作者的网站,以及插件的注释中到着全部问题的答案,我们可以转到jQuery的讨论列表。很多插件的作者都频繁的投稿而且始终乐意帮助解答新手将会遇到的问题。可以通过这个地址访问http://docs.jquery.com/Discussion


开发一个插件

第三方的插件提供了一群选项来提高我们的编程体验,但有时候我们需要走的更远一些。当我们写的代码可以被别人使用甚至被自己以后使用时,我们会想把它打包成一个插件。幸运的是,这个过程并不比写代码本身难到哪去。

增加新的全局函数

一些jQuery内建的功能是通过我们正在调用的全局函数提供的。正如我们所见,这些jQuery对象的方法,但是具体来说,他们都是jQuery命名空间下的函数。这项技术一个主要的例子就是$.ajax函数。所有$.ajax()做的事都可以被一个只是名为ajax()的常规全局函数完成,但是这种做法会让我们面对函数命名冲突。而把函数放在jQuery的命名空间里时,我们只需要担心是不是跟jQuery的其他方法冲突。

在jQuery的命名空间里增加一个函数,我们只需将一个新的函数赋值给jQuery对象的属性:
jQuery.foo = function() {
  alert('This is a test. This is only a test.');
};
那么在任何使用这个插件的代码里我们都可以写:
jQuery.foo();
我们也可以使用$别名来写:
$.foo();
这将会和其他任何函数一样调用运行,同时警告会被显示。

增加多个函数

如果我们的插件需要提供多个全局函数,我们可以独立的声明他们:
jQuery.foo = function() {
  alert('This is a test. This is only a test.');
};
jQuery.bar = function(param) {
  alert('This function takes a parameter, which is "' + param + '".');
};
那么两个方法都被定义了;所以我们可以用正常的形式调用他们:
$.foo();
$.bar('baz');
我们也可以稍微用$.extend()函数来整理以下函数定义:
jQuery.extend({
  foo: function() {
    alert('This is a test. This is only a test.');
  },
  bar: function(param) {
    alert('This function takes a parameter, which is "' + param + '".');
  }
});
这样得到的是同样的结果。虽然这里我们冒了不同命名空间混乱的风险。即使我们使用jQuery的命名空间来保护大部分的Javascript函数名和变量名,我们仍会遇到与其他jQuery插件的冲突。为了避免这些,最好是把插件所有的全局函数压进一个对象里:
jQuery.myPlugin = {
  foo: function() {
    alert('This is a test. This is only a test.');
  },
  bar: function(param) {
    alert('This function takes a parameter, which is "' + param +
'".');
  }
};
虽然我们仍然把这些函数看成好像是全局的,而现在在技术上他们已经是jQuery的全局函数了,所以我们调用这些函数的方法要稍微改变一下:
$.myPlugin.foo();
$.myPlugin.bar('baz');
运用了这项技术(和一个十分独特的插件名字),我们完全的从全局函数的命名空间冲突中被保护了。


要点是什么?

现在在我们的工具包(bag of tricks)里我们拥有了基本的插件开发。在把我们的函数储存为一个名为jquery.mypluginname.js的文件后,我们可以包含这段代码并且从使用页面里其他脚本的函数这些函数。但这与其他任何我们可以创建和包含的javascript文件有怎样的不同呢?

我们已经讨论过命名空间把我们的代码放在jQuery对象里面的好处。把我们的函数库写成一个jQuery的扩展的另一个好处而是:这些函数可以使用jQuery自身。通过把这些代码标记成插件,我们明确的需要在页面中包含jQuery。

[就算已经引入了jQuery,我们也不能相当然的认为$缩写符号是可以使用的。我们的插件应该始终使用jQuery或者在他们自身内部定义$来调用jQuery方法,正如后面所叙述的。]

虽然这些只是组织的好处。要真正给jQuery插件注入力量,我们还是需要了解怎样在单独的jQuery对象实例上创建方法。


添加jQuery对象方法

jQuery大多数内置的功能都通过他的方法来提供,这也是插件超群的地方。这适合于创建新的方法,无论一个函数是否需要在DOM的某部分上起作用。

我们已经见过,添加全局函数需要以新的方法扩展jQuery对象。添加实例方法是类似的,但我们通过扩展jQuery.fn对象来实现:
jQuery.fn.xyzzy = function() {
  alert('Nothing happens.');
}
[jQuery.fn 是jQuery.prototype的引用,提供了简写。]
我们可以从任何使用了选择器表达式后面的代码调用这个新的方法:
$('div').xyzzy();
当我们调用这个方法的时候我们的警告框就显示了。尽管我们写了一个全局函数,而我们没有以任何方式使用到匹配到的DOM节点。一个合理的方法处理按照它的内容来执行。


对象方法内容

在任何插件方法的内部,this关键字被设置为当前的jQuery对象。所以我们可以在this上调用任何内建的jQuery方法,或者选择它的DOM节点并使用他们:
jQuery.fn.showAlert = function() {
  alert('You called this method on "' + this[0] + '".');
}
但是我们要记住jQuery选择器表达式同样也可以匹配到0个,1个或者多个对象。我们必须在设计一个插件的方法时允许出现任何这类情况。而完成这件事最简单的办法就是总是在方法的内容里调用.each();这加强了固有的迭代,这种迭代保持了插件和内建方法之间的一致性。在.each()调用之内,这将轮流涉及到每一个DOM元素:
jQuery.fn.showAlert = function() {
  this.each(function() {
    alert('You called this method on "' + this + '".');
  });
}
现在我们的方法为每个用前面选择器表达式匹配到的元素制造了一个单独的警告提示框。


方法链

另外对于固有的迭代,jQuery用户应能够依靠连锁行为。这就意味着我们需要从所有插件的方法中都返回jQuery对象,除非这个方法有很明确的返回一块不同的信息的目的。返回的jQuery对象常常就是以this提供的那个。如果我们使用.each()来迭代this,我们可以就返回它的结果:
jQuery.fn.showAlert = function() {
  return this.each(function() {
    alert('You called this method on "' + this + '".');
  });
}
随着返回声明在适当的位置(with the return statement in place),我们可以用内建的方法链接我们的插件方法:
$('div').showAlert().hide('slow');


遍历DOM的方法

在某些情况下,我们的方法可能会改变,其中DOM元素是被jQuery对象引用的。例如,架设我们要增加一个DOM遍历方法来寻找匹配元素的祖父(父级的父级=爷级?)节点:
jQuery.fn.grandparent = function() {
  var grandparents = [];
  jQuery.each(this, function(index, value) {
    grandparents.push(value.parentNode.parentNode);
  });
  grandparents = $.unique(grandparents);
  return this.setArray(grandparents);
};
这个方法创建了一个新的祖父节点数组,(populating)通过迭代当前被jQuery对象引用的全部元素。内建的 .parentNode属性是用来寻找被压到这个数组中的祖父级元素的。这个数组被一个调用$.unique()删除了重复元素。然后jQuery的.setArray方法将这个匹配到元素的集合转换成了新数组。现在我们可以寻找并操作一个元素的祖父级节点了:
$('.foo').grandparent().addClass('bar');
不过,这个方法是具有破坏性的。实际的jQuery对象被修改成一个有显然的副作用的——如果我们用一个变量储存这个jQuery对象:
var $frood = $('.hoopy');
$frood.grandparent().hide();
$frood.show();
这段代码隐藏了祖父级元素,然后再显示出来。储存在#frood中的jQuery对象被改变为引用祖父节点。如果我们要把这个方法换成没有破坏性的代码,这种可能发生混淆的情况就不会出现了:
jQuery.fn.grandparent = function() {
  var grandparents = [];
  jQuery.each(this, function(index, value) {
    grandparents.push(value.parentNode.parentNode);
  });
  grandparents = $.unique(grandparents);
      return this.pushStack(grandparents);
};
相比改变原来的jQuery对象,.pushStact方法创建了一个新的。这就解决了我们遭遇的问题。现在,$frood.show()这行仍然引用原来的$('.hoopy')。而一方面的好处是,.pushStack()同样也允许.end方法运行在我们的新方法上,所以我们可以把方法合适的链接在一起:
$('.fred').grandparent().addClass('grandma').end().addClass('grandson');

[DOM遍历方法例如.children()在jQuery1.0中是毁灭性的,但在1.1中不再是了。]


方法参数

被传递给任何方法的最重要的参数就是关键字this,不过,我们当然可以定义额外的参数。为了使我们的插件API看起来尽可能的友善,我们把必须的参数放置在参数表的开始。而可选的参数也会被提供在参数列表中,对可选参数使用一个映射表常常更方便简单。

例如,架设我们的方法可以接受一个字符串和一个数字。我们可以定义方法接受两个参数:
jQuery.fn.myMethod = function(aString, aNumber) {
  alert('The string is "' + aString + '".');
  alert('The number is ' + aNumber + '.');
}
尽管这些参数使可选的,我们还是不得不解决四种可能的情况:
$('div').myMethod('hello', 52);
$('div').myMethod('hello');
$('div').myMethod(52);
$('div').myMethod();
我们可以检查来得知参数是否是定义了的和如果没有定义提供的默认值:
jQuery.fn.myMethod= function(aString, aNumber) {
  if (aString == undefined) {
    aString = 'goodbye';
  }
  if (aNumber == undefined) {
    aNumber = 97;
  }
  alert('The string is "' + aString + '".');
  alert('The number is ' + aNumber + '.');
}
这只在两个参数都存在,只给出了字符串或者都没有提供的情况下起作用。但是当那个数字是提供的而字符串不是的话,那个数字就被当作字符串传递进去了。因此我们需要检测参数的数据类型:
jQuery.fn.myMethod= function(aString, aNumber) {
  if (aString == undefined) {
    aString = 'goodbye';
  }
  if (aNumber == undefined) {
    if (aString.constructor == Number) {
      aNumber = aString;
      aString = 'goodbye';
    }
    else {
      aNumber = 97;
    }
  }
  alert('The string is "' + aString + '".');
  alert('The number is ' + aNumber + '.');
}
这在两个参数时还是便于管理的,但是当参数变的越来越多的时候很快就成为一个头疼的问题。为了避免这样所有的麻烦,我们可以用映射表来代替:
jQuery.fn.myMethod= function(parameters) {
  defaults = {
    aString: 'goodbye',
    aNumber: 97
  };
  jQuery.extend(defaults, parameters);
  alert('The string is "' + defaults.aString + '".');
  alert('The number is ' + defaults.aNumber + '.');
}
通过使用jQuery.extent(),我们可以很简单的提供默认值,无论提供什么样的参数,让他覆盖就是了。我们的方法调用仍然大致相同,除了使用一个映射表,而不是一个简单的参数表:
$('div').myMethod({aString: 'hello', aNumber: 52});
$('div').myMethod({aString: 'hello'});
$('div').myMethod({aNumber: 52});
$('div').myMethod();
这个策略要比数据类型检查要好的多。一方面的好处是,命名过的参数意味着添加新的可选项就不太可能破坏已有的代码,并且使用了插件的代码更具有自描述性。


添加新的快捷方法

jQuery类库必须在方便使用和复杂性之间维持一个微妙的平衡。每个添加到库里的方法可以帮助开发者编写某段代码时更快,但是增加了基础代码的总量,并且减少了实现(and reduce performance)。由于这个原因,很多内建函数的快捷方法都被移交给了插件,所以我们可以挑选出对每个项目有用的插件并省略不相关的。

当我们发现自己正重复一个惯用写法在我们的代码中好多次的时候,可能就提倡创建一个快捷方法了。jQuery的核心库里包含一些这样的快捷方法,正如.click()简化了.bind('click')。这些插件创建很简单,因为他们只需沿着核心方法传递参数并提供一些我们自己的。

例如,假设我们频繁的动画显示某些项目,使用一个内建的“slide”和“fade”技术的联合体。把这些效果放在一起的意思是在一个动画里同时改变一个元素的高度和透明度。.animate()方法使这个变得简单:
.animate({height: 'hide', opacity: 'hide'});
当显示和隐藏元素的时候,我们可以创建一对快捷方法来演绎这个动画:
jQuery.fn.slideFadeOut = function() {
  return this.animate({height: 'hide', opacity: 'hide'});
}
jQuery.fn.slideFadeIn = function() {
  return this.animate({height: 'show', opacity: 'show'});
}
现在我们可以调用$('.myClass').slideFadeOut()并在任何需要的地方触发这个动画。因为,在一个插件方法的定义之内,this指向当前的jQuery对象,这个动画将在全部匹配到的元素上都执行一次。

为了更完整,我们的新方法应该支持原来内建快捷方法所支持一样的参数。特别的,形如.fadeIn()之类的方法可以被自定义速度和回调函数。因为.animate()也使用这些参数,允许这个是很直白的。我们只要接收参数并进一步传递给.animate():
jQuery.fn.slideFadeOut = function(speed, callback) {
  return this.animate({height: 'hide', opacity: 'hide'}, speed, callback);
}
jQuery.fn.slideFadeIn = function(speed, callback) {
  return this.animate({height: 'show', opacity: 'show'}, speed, callback);
}
现在我们拥有的自定义快捷方式就运行起来就像他们的复制品。


保持多样性的事件日志

作为一个javascript开发人员,我们会发现当各种各样的事件发生时我们有显示日志的需要。Javascript的alert()函数是经常被使用来演示,但我们需要在一些场合下不允许频繁的,及时信息。一个更好的选择是在Firefox和Safari上提供的console.log()函数,允许把消息打印在一个独立的日志里,而不会打断页面上的交互流程。因为这个函数在Internet Explorer上没有,不过我们可以使用一个自定义的函数来完成这种风格的消息日志。

[Firebug Lite script(叙述在附录B中)提供了一个非常有力的跨平台日志工具。这里我们开发的方法是为了普遍通用的情况量身定制的,尽管Firebug是个更具代表性的。]

一个简单的记录消息的方法应该是创建一个全局函数,在页面的一个特定元素中添加消息:
jQuery.log = function(message) {
  $('<div class="log-message" />').text(message).appendTo('.log');
};
我们甚至可以发挥点想象,让新的消息以动画的形式出现:
jQuery.log = function(message) {
  $('<div class="log-message" />')
    .text(message)
    .hide()
    .appendTo('.log')
    .fadeIn();
};
现在我们可以调用$.log('foo')来显示foo在页面的日志盒子里。

有些时候我们在一个单独的页面上有多种多样的实例,然而,为每个实例保持独立的日志是非常简便的。我们可以通过使用一个方法完成这一点,而不是一个全局函数:
jQuery.fn.log = function(message) {
  return this.each(function() {
    $('<div class="log-message" />')
      .text(message)
      .hide()
      .appendTo(this)
      .fadeIn();
  });
};
现在调用$('.log').log('foo')在我们的全局函数调用以前就起作用了,而我们可以改变选择器表达式来把目标放在不同的日志盒子里。

理想的说,尽管在没有一个精准的选择器的情况下,.log方法是足够智能来定位用于日志消息最有关的盒子的。利用传递给这个方法的上下文,我们可以遍历DOM来找到离选择到的元素最近的日志盒子:
jQuery.fn.log = function(message) {
  return this.each(function() {
    $context = $(this);
    while ($context.length) {
      $log = $context.find('.log');
      if ($log.length) {
        $('<div class="log-message" />').text(message).hide()
                                            .appendTo($log).fadeIn();
        break;
      }
      $context = $context.parent();
    }
  });
};
这段代码以匹配的元素查找一个日志消息盒子,并且如果一个没有找到的话,就沿着DOM网上搜索之一。

最后,我们需要显示一个对象内容的能力。打印出这个对象本身产出(yields?)的一些勉强翔实的东西像[object object],因此我们可以检测到参数的类型并在一个对象传递进来的情况下做些我们自己的进一步处理(pretty-printing):
jQuery.fn.log = function(message) {
  if (typeof(message) == 'object') {
    string = '{';
    $.each(message, function(key, value) {
      string += key + ': ' + value + ', ';
    });
    string += '}';
    message = string;
  }
  return this.each(function() {
    $context = $(this);
    while ($context.length) {
      $log = $context.find('.log');
      if ($log.length) {
        $('<div class="log-message" />').text(message).hide()
                                            .appendTo($log).fadeIn();
        break;
      }
      $context = $context.parent();
    }
  });
};
现在我们拥有了一个方法,可以用来在是相关工作所完成的页面的一个地方都写出来的对象和字符串。


增加一个选择器表达式

jQuery内建的部分也可以被扩展。比起添加新的方法,我们甚至可以定制现有的。一个普遍的需求,例如扩展jQuery提供的选择器表达式来给出更多的专家级(esoteric,内行的,悔涩的)选项。

jQuery作为工具的:nth-child()伪类允许我们通过给出的他们在父级元素中所在的位置找到项目。架设我们构造一个10个项目的顺序列表:
<ol class="nthchild">
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
  <li>Item</li>
</ol>
表达式$('li:nth-child(4)')会定位在列表中的第四个元素。我们在前面就见过这样的功能。然而,这个选择器所基于的CSS规范更强大一些。在CSS 3里,:nth-child()伪类是能够不只以整数作为参数,还可以是任何形如an+b的表达式。如果一个元素的位置刚好等于这个表达式或者任何n整数值,那么这个元素就会被匹配到。例如,:nth-child(4n+1)会匹配第1,5,9等等元素。我们可以使用一个插件往jQuery的选择器引擎里增加这个功能。

jQuery选择器解析器首先使用一个正则表达式几何分解选择器表达式。对于选择器的每一块,一个函数的执行分析出可能匹配到的节点。这个函数可以在jQuery.expr表中找到。我们可以通过$.extent()覆盖内建的:nth-child()伪类的行为:
jQuery.extend(jQuery.expr[':'], {
  'nth-child': 'jQuery.nthchild(a, m)',
});
这个表的值包含的都是字符串,这些字符串是用来过滤元素的JavaScript表达式的。在这些表达式中,a引用了DOM元素以待检测,而m是一个保存选择器组成部分的数组。

m的准确内容使依赖我们提供(implementing)的格式化的选择器变的多样化,所以我们第一步就是用jQuery.js里的jQuery.parse检查整个正则表达式。看那些匹配出来的,我们可以看到对于:x(y(z))格式的伪类,在m中的成份会是:
m[0] == ':x(y(z))'
m[1] == ':'
m[2] == 'x'
m[3] == 'y(z)'
m[4] == '(z)'
我们的代码对于:nth-child()伪类调用了一个在jQuery命名空间里叫做nthchild()的函数,也就是我们做了大量事情(heavy lifting 举重)(利用这个机会来分别的用a和m来改变更容易理解的元素和成份的名称)的地方:
jQuery.nthchild = function(element, components) {
  var index = $(element).parent().children().index(element) + 1;
  var numbers = components[3].match(/((\d+)n)?\+?(\d+)?/);
  if (numbers[2] == undefined) {
    return index == numbers[3];
  }
  if (numbers[3] == undefined) {
    numbers[3] = 0;
  }
 
  return (index - numbers[3]) % numbers[2] == 0;
}
首先这个函数寻找当前节点在兄弟节点中的索引。这个操作如果使用纯的DOM遍历函数会更快一些,但是这里使用jQuery的方法我们可以使代码更可读一些。我们可以给结果+1,因为CSS说明了:nth-child()伪类是基于1的而不是基于0的。

一旦我们找到这个索引,我们就把原始表达式分成几个部分。一个如4n+1的表达式会被分成numbers[2]是4而number[3]是1。我们增加一些特殊情况来处理像4n和1这样的表达式。

最后,我们做一些代数操作来找到如果是an+b=i,那么(i-b)/a=n。这说明了一个我们可以操作的计算来决定一个给出的索引是否能通过测试。如果这个元素应该是结果集的一部分的话,我们返回true;不是的话返回false。

有了我们新安装的插件,我们现在可以使用像$('li:nth-child(3n+2)')这样的jQuery选择器并可以很容易的在列表里找到每一个数到三的元素,以#2元素开始。


创建一个缓冲(easing)样式

当我们调用一个动画方法,我们要指明我们需要动画改变每个属性的起点和终点。我们同样也可以告诉方法从A点到B点要多快。而我们并没有提供任何以哪种方式从A点到B点的指示。动画不一定是在恒定速率,并且实际上默认就不是(by default is not)。

考虑一个元素从左到右的动画,并随之淡出他的透明度:
$('.sprite').animate({'left': 791, 'opacity': 0.1}, 5000);
如果我们观察动画进行并甚至在时间间隔时对元素的位置抓图(capture),我们就对他在运行中的速度有一个了解:

[图]

我们可以看到从这个示例里,动画启动的很慢,大部分(bulk of)持续时间加速,并在快结束时再次减速。实际的演示一个非恒定速率的动画就叫做缓冲。这个默认的缓冲样式叫做swing(摆动),比起一个匀速的运动来,感觉更自然而不是那么生硬。

我们可以改变缓冲的样式,通过使用一个jQuery动画,其中提供了一个额外的参数给.animate()方法。这个参数决定了哪个缓冲函数会被使用。这个唯一的在jQuery中内建的函数是默认的,也就是刚刚我们所看到的;要使用其他的,我们必须从一个插件获得或者由我们自己编写。

增加新的缓冲函数是和增加新的选择器表达式相似的。我们扩展了全局的jQuery对象来给他的缓冲增加属性。每个属性与一个单独的缓冲函数所对应。

例如,架设我们要实现一个真实的线性缓冲样式,使动画从开始到结束都运行在一个恒定速率。我们可以用一个单行的缓冲函数完成这些:
jQuery.extend({
  'easing': {
    'linear': function(fraction, elapsed, attrStart, attrDelta,
                                                         duration) {
      return fraction * attrDelta + attrStart;
    }
  }
});


缓冲函数的参数

所有的缓冲函数都有接收下面5个参数:
 fraction:当前动画的位置,一个在0(动画开始)到1(动画结束)之间的时间量
 elapsed:从动画开始已经过去的时间毫秒数(很少使用到)
 attrStart:起始的CSS属性值
 attrDelta:起始与结束之间的CSS属性值的差异
 duration:动画过程要运行时间的总毫秒数

缓冲函数被期望通过这5个参数来计算一个数字,这个数字表示在每一个给出的时间动画运行时参数应该是什么值。例如,架设我们正使用我们的线性缓冲函数来改变一个元素的高度,从20象素到30象素:

[图表]

在这个简单的例子中,我们可以仅仅用attrDelta乘以fraction来得出参数所改变到现在的距离增量。注意到elapsed的值是从0到duration的,fraction总是等于elapsed/duration,而且函数值从attrStart变化到attrStart+attrDelta。

我们现在可以通过新的缓冲样式重复我们的动画:
$('.sprite').animate({'left': 791, 'opacity': 0.1}, 5000, 'linear');
使用这个缓冲函数,我们动画的定时截图给出了一个不同的图片:

[图]

动画现在就是以匀速进行的了。


多个部分的缓冲样式

如果对动画有稍微更多的兴趣,我们可以手工设计一个缓冲函数,让其在运动的每个不同(时间)部分遵循不同的曲线:
jQuery.extend({
  'easing': {
    'back-n-forth': function(fraction, elapsed, attrStart, attrDelta, 
                                                         duration) {
      if (fraction < 0.33)
        return fraction * (1.0 / 0.33) * attrDelta + attrStart;
      if (fraction < 0.66)
        return (-fraction + 0.66) * (1.0 / 0.33) * attrDelta + 
                                                           attrStart;
      return (fraction - 0.66) * (1.0 / 0.34) * attrDelta + attrStart;
    }
  }
});
这个函数把动画打断成三个想等的部分,每一个都遵循一个线性运动。我们可以用前面相同的方式测试这个缓冲样式:
$('.sprite').animate({'left': 791, 'opacity': 0.1}, 5000, 'back-n-forth');
这个动画的效果会呈现为向前,向后再次向前:
[图]
现在建立更复杂的缓冲效果最重要的一件事情就是找到我们需要让生成的曲线所遵循的数学表达式,并且把这个表达式编码成Javascript。

很多缓冲函数是已经在现有的插件中的了,如Interface。


怎样做一个好公民

在编写插件时为了和不影响(play well)其他的代码运行,有几条规则需要遵循。有的我们在前面已经提到过,不过为了方便再次把他们收集在这。


命名习惯

所有的插件文件必须命名为jQuery.myPlugin.js,其中myPlugin就是插件的名字。在文件中,所有的全局函数都应该以一个叫jQuery.myPlugin的对象作为分组,除非只有一个函数,在这种情况下他可以是一个叫做jQuery.myPlugin()的函数。

方法名则更灵活些,但也应该尽量保证唯一。如果只定义了一个函数,那应该叫做jQuery.fn.myPlugin()。如果多于一个,尽量尝试给每个方法名加上插件名的前缀,以防止冲突。避免短的,不明确的方法名如.load()或.get(),这可能会与其他插件定义的方法名相冲突。


$别名的使用

jQuery插件可能不能保证$别名是可用的。相反,jQuery全明必须每次都写。

在更长的插件里,很多开发者发现了$缩写的缺陷,这使得代码更加难以阅读。别名可以通过定义并执行一个函数只在插件范围的局部定义。定义并一次执行一个函数的语法应该是这样的:
(function($) {
  // Code goes here
})(jQuery);
封装的函数只使用一个单独的参数,他将给我们传递全局的jQuery对象。参数名就是$,所以在函数内部我们就可以没有冲突的使用$别名了。


方法的接口

所有的jQuery方法都是被一个jQuery对象调用的,所以this指向的一个对象可能包含了一个或更多的DOM元素。所有方法必须做到正确,无论实际匹配到多少个元素。一般来说,方法应该调用this.each()来迭代全部匹配到的元素,对每一个轮流操作。

方法应该返回jQuery对象以保持(方法)链。如果一个匹配到的对象集合被编辑过,应该通过调用.pushStack()创建一个新的对象,而且返回的应该是这个新对象。如果返回的不是一个jQuery对象,那么必须写明注释。

方法的定义必须以一个分号结尾,这样代码压缩器才能正确地解析文件。


注释(documentation)风格

文件内部的注释应该以ScriptDoc格式写在定义每个方法前,格式可以参见http://www.scriptdoc.org/


总结

在这最后一章里,我们看到了jQuery核心提供的功能性是如何不限制类库的实际能力的。现成的插件充分的扩展了(所列在)目录(里)的特性,而且我们可以轻松的创建我们自己的,扩展(push the boundaries)的更远。

我们研究了Dimensions插件,一个用来测量和处理元素大小的。Form插件对于HTML表单交互也是非常有用的。我们也学习了Interface插件,增加(enabling)了各种用户界面控件。

我们也学会了怎样创建包含丰富特性的插件,包括使用jQuery类库的全局函数,新的作用在DOM元素上的jQuery对象的方法,以新的方式增强寻找DOM元素的选择器表达式,和可以改变动画速率的缓冲函数。

在我们使用这些工具布署时,我们可以把jQuery——以及我们自己的Javascript代码——变成任何我们期望的形式。

转自:http://mytharcher.blog.163.com/blog/static/2081820200781094415905/

posted @ 2008-05-18 22:52  hehuachina  阅读(315)  评论(0)    收藏  举报