jQuery 入门指南(三)
九、编写 jQuery 插件
jQuery 插件是初学者倾向于回避或害怕使用的东西。在人们的脑海中,插件似乎是作为难以置信的复杂事物来使用的,但一旦你了解了它们的工作方式,你会发现它们实际上非常简单,你会发现自己在工作的同时制作多个插件。正如本章将要演示的那样,插件并不像您想象的那么复杂。
在探索了插件的好处以及何时将代码转化为插件之后,您将了解不同类型的插件,构建几个小插件来看看它们是如何工作的,并发现如何使它们对潜在的开发人员来说更具可配置性。在下一章中,你将获取你之前编写的代码,包括你在第五章中开始的 accordion 和第八章中的 Ajax 代码,并将它们转换成功能完整的插件。然后,您将编写一个功能全面、复杂的图像滑块插件。但现在,我们会保持简单。
为什么是插件?
在编写代码时,您会注意到一些典型的模式或迹象,值得将其抽象到插件中。如果您发现自己在想象另一个项目或情况,其中您编写的一些代码可以被重用,这是一个好迹象,表明您应该将它变成一个插件。你的手风琴代码就是一个很好的例子。
如果您发现自己在不同的项目中多次编写非常相似的代码,这是一个很好的迹象,表明您应该花时间来开发一个插件,然后可以轻松地重用它。
如果您发现自己不止一次地编写代码来构建一个简单的手风琴,那么是时候停下来构建一个插件了。最初可能会花一点时间,但之后您就有了一个很好的、自包含的插件,您再也不用编写它了。写一次,受益多次。
到目前为止,我们已经在手风琴的上下文中讨论了插件,但是插件确实可以为您经常做的任何事情而制作。它可以是一个只有三行代码的小插件,只是为了让某个动作变得更加简单,也可以是一个复杂的图像滑块那么大。
您的第一个 jQuery 插件
您的第一个 jQuery 插件将会非常简单。它只是为每个被调用的元素记录下ID属性的值。
在编写插件之前,想象一下另一个开发人员将如何使用您的插件是很有用的。在这种情况下,设想执行以下操作:
$("div").logId();
您应该能够打开您的浏览器控制台,看到$("div")返回的每个元素的日志语句。日志应该只是元素的ID属性,如果元素没有属性,则为空。
了解如何创建它的最佳方式是深入并开始编写代码,一边编写一边解释。因此,创建一个新的目录,可能叫做logid-plug-in或类似的目录,并在其中放一些文件。首先,创建index.html:
<!DOCTYPE html>
<html>
<head>
<title>Chapter 09, Exercise 01</title>
<script src="jquery.js"></script>
<script src="logid.jquery.js"></script>
<script src="app.js"></script>
</head>
<body>
<div id="div1">Hello</div>
<div id="div2">Hello</div>
<div id="div3">Hello</div>
<div id="div4">Hello</div>
<div id="div5">Hello</div>
<div id="div6">Hello</div>
<div id="div7">Hello</div>
</body>
</html>
将 jQuery 源代码也放在目录中一个名为jquery.js的文件中。最后,再创建两个空白文件。第一个应该叫做logid.jquery.js,它将存放您的插件,第二个叫做app.js,在这里您将编写利用插件的代码。将您的插件文件命名为name.jquery.js是惯例,因此您将坚持使用它。
我们将向您展示插件的实现,然后详细解释它。下面是获得您的logId插件的完整代码,您应该将它放入logid.jquery.js:
$.fn.logId = function() {
return this.each(function() {
console.log(this.id);
});
};
下面是你应该输入app.js来测试它的内容:
$(function() {
$("div").logId();
});
您的控制台中的结果应该如图 9-1 所示。
img/A310335_2_En_9_Fig1_HTML.jpg)
图 9-1。
The output from using the plug-in (on the Chrome developer console)
退一步想清楚这里到底发生了什么。第一行将您的方法logId添加到 jQuery。这也称为扩展 jQuery:
$.fn.logId = function() {...});
向$.fn方法添加一个方法意味着它可以用于 jQuery 对象。记住,jQuery 对象是运行$("div")或类似操作的结果。这一行所做的只是向$.fn、logId添加一个新的属性,它相当于一个函数。
下一个有趣的部分是each()方法:
return this.each(function() {...});
在函数中,this的值是调用函数的 jQuery 对象。所以当你调用$("div").logId()的时候,this的值指的是包含$("div")结果的 jQuery 对象,它将包含页面上所有的div。因此,您希望循环遍历this中的每个元素,这样您就可以在每个元素上运行您的代码。
另一件需要注意的重要事情是,您返回了循环的结果。当循环某个东西时,最初的“东西”总是会被返回。所以当你跑步的时候:
return this.each(function() {...});
在它的结尾,this被返回。这意味着您的插件是可链接的,或者有人可以在您的插件之后调用另一个方法,例如:
$("div").logId().fadeOut();
除非您的插件是专门设计来返回特定值的,否则它应该总是可链接的。人们会认为你的插件是可链接的——如果不是,他们会非常困惑。真的没有借口不使它可链接。
最后,在循环中,this的值指的是正在循环的单个 DOM 元素。注意this不是 jQuery 对象,而只是 DOM 元素。如果你想在上面调用 jQuery 方法,你必须先运行$(this)。
在循环中,您需要做的就是获取元素的 ID。您可以执行以下操作:
$(this).attr("id");
但是实际上,不需要 jQuery 就可以很容易地获得 DOM 元素的 ID:
this.id
这实现了同样的事情,并且比 jQuery 对等物更精简,所以没有理由不这样做。因此,您现在有了以下内容:
$.fn.logId = function() {
return this.each(function() {
console.log(this.id);
});
};
恭喜你!您已经编写了第一个 jQuery 插件!
改进
尽管如此,这个插件仍然存在一些问题。首先要解决的是如何定义插件的问题。目前是这样做的:
$.fn.logId = function() {...});
但是,不能保证$变量指的是jQuery。有时使用 jQuery 的人不允许它使用$变量,因为另一个脚本或插件可能正在使用它。当然,这不太可能,但是您永远不应该假设$是 jQuery。通过 jQuery 的noConflict()方法( http://api.jquery.com/jQuery.noConflict/ ),很容易让 jQuery 发布$。这里有一个例子:
jQuery.noConflict();
// now, $ is reset to whatever it was before jQuery was loaded
jQuery("div");
jQuery 默认存在两个全局变量:$和jQuery。它们都是相同的,所以如果调用了jQuery.noConflict(),您仍然可以通过jQuery方法使用 jQuery,但是不能通过$。
为了安全起见,您应该在插件中使用jQuery变量,而不是$:
jQuery.fn.logId = function() {
return this.each(function() {
console.log(this.id);
});
};
但是输入jQuery而不是$是令人恼火的。谢天谢地,有更好的方法来做事。
一种方法是创建一个函数来定义你的函数。你要做的第一件事就是设置$等于jQuery:
var definePlugin = function() {
var $ = jQuery;
$.fn.logId = function() {
return this.each(function() {
console.log(this.id);
});
};
};
definePlugin();
这是可行的,因为函数中定义的变量只在该函数中可用,所以您可以安全地定义$而不用全局定义它。你可能不喜欢这个解决方案,尽管它很有效。定义然后调用一个函数似乎是一种冗长的做事方式。如果有一种方法可以定义并立即执行一个函数就好了…
立即调用的函数表达式
术语“立即调用的函数表达式”(IIFE)是由多产的 JavaScript 开发人员 Ben Alman 在他的个人博客中创造的,他在一篇关于定义然后立即调用的函数的文章中( http://benalman.com/news/2010/11/immediately-invoked-function-expression/ )。这篇文章非常值得一读,但是非常深入,所以当您对 jQuery 更加熟悉,并且希望了解更多关于 JavaScript 的细节时,您可能会想要阅读它。
生活是一个被立即定义然后执行的功能。尝试在浏览器中加载 JS 控制台,然后运行这行代码:
(function() { console.log("hey"); })();
你将会看到“嘿”这个词正对着你。您只是定义了一个函数——尽管是一个匿名函数(您从未给它起名)——并执行它,所有这些都在同一行中。正如您可能已经猜到的,这里的关键是行尾的一对括号和函数定义周围的一对括号。
函数周围的括号是由于 JavaScript 的解析器是如何工作的。如果你尝试:
function() { console.log("hey"); }
您将得到一个语法错误:
SyntaxError: Unexpected token (
这是谷歌 Chrome 提供的语法错误;每个浏览器的做法略有不同。例如,Firefox 会声明“SyntaxError: function 语句需要一个名称。”这是因为解析器认为您正在定义一个新函数,因此期望在关键字function和左括号之间有一个名字。它认为你在声明一个函数。因此,这不起作用,因为您将得到相同的错误:
function() { console.log("hey"); }()
用括号把它括起来,告诉解析器它是一个表达式而不是声明,这意味着它不需要名字,而是把这一行作为函数表达式来解析;换句话说,它评估函数的内容。第二对空括号只是调用新定义的函数。
那么这和最初的问题有什么关系呢?嗯,生命也可以带参数。在您的控制台中尝试:
(function(x) { console.log(x); })("hey");
这里定义一个函数,它接受一个参数x,并记录它。当你调用它的时候,你可以传入这个值。和这样做没什么区别:
function log(x) {
console.log(x);
};
log("hey");
现在你知道你可以做到这一点,你应该能够看到它如何适用于最初的问题。您可以定义函数,立即执行它,并更简洁地传入 jQuery:
(function($) {
// our plugin here
})(jQuery);
在那个函数内,你可以使用$作为jQuery,不管它在函数外是否被定义为jQuery。这样,您的插件变得更加健壮:
(function($) {
$.fn.logId = function() {
return this.each(function() {
console.log(this.id);
});
};
})(jQuery);
Note
IIFEs 是本书中使用的 JavaScript 中最复杂的部分之一。如果你在这之后对他们感到舒服,你做得真的很好。如果您需要不止一次地阅读这一部分,请不要担心;生活的方式并不简单。如果你想了解更多信息,最好的资源是本·阿尔曼在 http://benalman.com/news/2010/11/immediately-invoked-function-expression/ 的生活文章。
给用户提供选项
当制作一个插件时,让用户选择它在某个地方如何工作可能是有益的。您可以让用户选择文本颜色,或者如果您的插件为 DOM 元素制作动画,可以让用户选择动画的速度。
现在,您将创建另一个类似于第一个插件的小插件。这将是一个记录到控制台的功能,但记录任何属性。因此,您需要让用户传入他们想要记录的属性。
创建一个新目录,可能是logattribute-plug-in,并使用下面的代码创建一个新的index.html文件。它与您之前制作的插件中的 HTML 相同,只是您的插件的文件名不同。
<!DOCTYPE html>
<html>
<head>
<title>Chapter 09, Exercise 02</title>
<script src="jquery.js"></script>
<script src="logattr.jquery.js"></script>
<script src="app.js"></script>
</head>
<body>
<div id="div1">Hello</div>
<div id="div2">Hello</div>
<div id="div3">Hello</div>
<div id="div4">Hello</div>
<div id="div5">Hello</div>
<div id="div6">Hello</div>
<div id="div7">Hello</div>
</body>
</html>
创建一个空白的app.js文件和一个logattr.jquery.js文件,其中应该包含以下代码,这是您的插件的基础:
(function($) {
$.fn.logAttr = function() {
};
})(jQuery);
您将再次使用围绕您的插件代码的生命,从现在开始,您将对所有插件都这样做。
首先尝试实现以下内容:
(function($) {
$.fn.logAttr = function(attr) {
return this.each(function() {
console.log($(this).attr(attr));
});
};
})(jQuery);
您只需定义一个将属性作为参数的函数,然后在循环中使用attr()方法将其注销。为了进行测试,将这段代码放到app.js中,然后在浏览器中加载index.html:
$(function() {
$("div").logAttr("id");
});
如果你检查控制台,你会看到每个div的 ID 被记录。
假设您正在为一个客户构建这个,他带着以下请求回来:
- 如果元素没有指定属性,他们希望能够定义一个备份值来记录。
- 他们希望能够在使用控制台和简单地警告变量之间切换,因为他们需要测试的一些浏览器没有控制台。
这都是合理的要求。先整理一下备份值。它作为另一个参数是有意义的,所以添加它。然后,如果属性存在,您可以注销它,如果不存在,您可以注销备份值。让我们用下面的例子:
(function($) {
$.fn.logAttr = function(attr, backup) {
return this.each(function() {
console.log($(this).attr(attr) || backup);
});
};
})(jQuery);
这是关键的一行:
console.log($(this).attr(attr) || backup);
它之所以有效,是因为如果元素没有属性,就会返回undefined。然后,语句变成
undefined || backup
undefined被评估为假,那么backup被评估并返回。
为了测试这一点,向其中一个div添加一个rel属性:
<div id="div1" rel="someDiv">Hello</div>
然后将app.js的内容改为
$(function() {
$("div").logAttr("rel", "N/A");
});
当您在浏览器中运行时,您会看到"someDiv"记录了一次,而"N/A"记录了六次。
接下来,向插件添加另一个选项,如果设置为 true,将使用alert而不是console.log:
(function($) {
$.fn.logAttr = function(attr, backup, useAlert) {
return this.each(function() {
if(useAlert) {
alert($(this).attr(attr) || backup);
} else {
console.log($(this).attr(attr) || backup);
}
});
};
})(jQuery);
调用插件时,只需添加true作为第三个参数:
$(function() {
$("div").logAttr("rel", "N/A", true);
});
然而,这开始变得混乱。更好的选择是让最终用户传入一个键值对对象,这是您想要的选项。你很快就会这么做。首先,您可以做一些重构:
if(useAlert) {
alert($(this).attr(attr) || backup);
} else {
console.log($(this).attr(attr) || backup);
}
这里你重复了两次$(this).attr(attr) || backup。最好将它保存到一个变量中:
var val = $(this).attr(attr) || backup;
if(useAlert) {
alert(val);
} else {
console.log(val);
}
但是,这可以用三元运算符进一步缩短:
var val = $(this).attr(attr) || backup;
useAlert ? alert(val) : console.log(val);
这是一个更好的实现。
向插件添加选项
我们不喜欢用户配置插件的方式。在我们看来,以下是凌乱的:
$.fn.logAttr = function(attr, backup, useAlert)
如果用户想将useAlert设置为true,但将backup保留为默认值(即undefined,他们必须像这样调用插件:
$("div").logAttr("rel", undefined, true);
这是令人难以置信的混乱。您应该只让用户在不想使用默认值时指定选项。这就是使用对象存储选项的好处。因此,您可以让用户像这样调用插件:
$("div").logAttr({
attr: "rel",
useAlert: true
});
这是你在本章中要做的下一件事,在完成你的logAttr插件并使用你之前写的代码查看 accordion 插件之前。
就实现传入对象的能力而言,有三个步骤:
- 创建一个包含选项的对象,设置为默认值。
- 允许用户传入包含所需设置的对象。
- 用用户选项覆盖您的默认值。
前两步非常简单。首先,更改函数,使其接受一个参数,即 options 对象:
$.fn.logAttr = function(opts) {
然后,在该函数中,定义默认值:
var defaults = {
attr: "id",
backup: "N/A",
useAlert: false
};
下一步是采用默认值并用用户传入的选项覆盖它们。插件经常这样做,因此 jQuery 提供了一个名为$.extend ( http://api.jquery.com/jQuery.extend/ )的实用方法来做这件事。它有几个用例,但是您使用的主要用例是它可以接受两个对象并将它们合并成一个对象。如果在两个对象中都定义了一个属性,则后一个对象优先。以这两个物体为例:
var objectOne = { x: 2, y: 3, z: 4 };
var objectTwo = { x: 4, a: 5 };
然后调用$.extend,传入这些对象:
$.extend(objectOne, objectTwo);
这将使用objectTwo并将其合并到objectOne,修改它以包含合并对象的结果。在这种情况下,objectOne会是这样的:
{ x: 4, y: 3, z: 4, a: 5 }
注意,因为x存在于objectTwo中,所以它被用在了存在于objectOne中的x之上。
通常,您不会想要修改任何一个对象,而是从合并的结果创建一个新的对象。这可以通过将一个空白对象作为第一个参数传递给$.extend来实现。$.extend的返回值始终是合并后的对象。
var objectOne = { x: 2, y: 3, z: 4 };
var objectTwo = { x: 4, a: 5 };
var merged = $.extend({}, objectOne, objectTwo);
这为您留下了一个新对象merged,它包含:
{ x: 4, y: 3, z: 4, a: 5 }
但是,objectOne和objectTwo没有被改动。您在插件中使用的就是这种用法。
在您的插件中实现这一点,您最终会得到以下结果:
(function($) {
$.fn.logAttr = function(opts) {
var defaults = {
attr: "id",
backup: "N/A",
useAlert: false
};
var options = $.extend({}, defaults, opts);
return this.each(function() {
var val = $(this).attr(options.attr) || options.backup;
options.useAlert ? alert(val) : console.log(val);
});
};
})(jQuery);
定义默认值后,使用$.extend将用户传入的对象opts合并到默认值中,并创建一个新对象。$.extend返回这个值,所以你把它存储到options,一个新的变量。
之前在您的选项中,您将用户选项称为attr或backup;现在他们存在于options内部,所以不得不被称为options.attr和options.backup。
现在您的插件的用法已经改变,所以进入app.js并更新它:
$(function() {
$("div").logAttr({
attr: "rel"
});
});
使用期权的好处现在应该很明显了。对于您正在设置哪些选项,这要清楚得多,因为您是通过键/值对对象来设置它们的。此外,您只需指定不同于默认设置的选项。
现在你已经创建了一个稍微复杂一点的函数,是时候重温你在第五章写的手风琴代码,并把它变成一个插件了。
手风琴式插件
让我们重温一下你为手风琴写的代码。您可以创建一个新目录或复制以前的目录。以下是app.js的内容:
$(function() {
var accordion = $("#accordion");
var headings = $("h2");
var paragraphs = $("p");
var animateAccordion = function(elem, duration, easing) {
paragraphs.stop(true, true).slideUp(duration, easing);
$(elem).stop(true, true).slideDown(duration, easing);
}
paragraphs.not(":first").hide();
accordion.on("click", "h2", function() {
var t = $(this);
var tPara = t.next();
if(!tPara.is(":visible")) {
tPara.trigger("showParagraph");
}
});
accordion.on("showParagraph", "p", function() {
animateAccordion(this, 600, "easeInCirc");
});
});
这是来自index.html的 HTML:
<!DOCTYPE html>
<html>
<head>
<title>Chapter 09, Accordion Plugin</title>
<script src="jquery.js"></script>
<script src="app.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div id="accordion">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>Heading 2</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>Heading 3</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
</body>
</html>
在style.css中你也有少量的 CSS:
#accordion {
width: 500px;
border: 1px solid black;
}
#accordion h2 {
padding: 5px;
margin: 0;
background: #ddd;
}
#accordion p {
padding: 0 5px;
}
在最初的 accordion 中,您还包含了 jQuery UI 源代码,因此您可以使用不同的缓动类型。这次你不会把它包括进去。只是简单地使用默认动画。
开始把它变成一个插件。插件的一个可能用途是在包含所有内容的div上调用它。然后,您必须向它传递用于标题和内容的元素类型。例如,你可以这样称呼它:
$("#accordion").accordion({
headings: "h2",
content: "p"
});
创建一个新文件accordion.jquery.js,并将其留空。如果您愿意,您可以加载旧的 accordion 代码作为参考。更改app.js,让它使用您的插件:
$(function() {
$("#accordion").accordion({
headings: "h2",
content: "p"
});
});
很明显,这不会马上生效,但是你现在的任务是让它生效。你也应该编辑index.html,在app.js之前添加新的accordion.jquery.js文件:
<script src="jquery.js"></script>
<script src="accordion.jquery.js"></script>
<script src="app.js"></script>
现在你可以开始了。在插件文件(accordion.jquery.js)的顶部,写下初始模板:
(function($) {
$.fn.accordion = function(opts) {
var defaults = {
headings: "h2",
content: "p"
};
var options = $.extend({}, defaults, opts);
};
})(jQuery);
这里你使用一个生命来确保你可以使用$并且它指向jQuery。创建函数,在设置默认选项之前,它将接受一个参数—选项的对象。你可能需要更多的选择;如果有,你可以边走边添加。然后,创建最终的对象集,将用户的选项与默认值合并。
接下来,您将设置包含您的功能的循环。这在插件被调用的每个元素上循环。请记住,您返回它以便您的插件是可链接的。在循环中,您可以通过this获取当前元素,并因此创建一个$this变量,该变量引用$(this),它是 jQuery 对象中包装的当前元素。如果您正在处理一个有很多变量的项目,其中一些引用 jQuery 对象,而另一些不引用,那么可以考虑在引用的变量前面加上一个$作为自己的视觉提示,这些变量是 jQuery 对象。
return this.each(function() {
var $this = $(this);
});
然后,您需要存储对标题和内容的引用。在最初的手风琴中,您执行了以下操作:
var headings = $("h2");
var paragraphs = $("p");
但是在这种情况下,选择器是在选项中定义的。您还应该将搜索限制在手风琴范围内:
return this.each(function() {
var $this = $(this);
var headings = $this.children(options.headings);
var paragraphs = $this.children(options.content);
});
如果你看看最初的手风琴,你有一个小的实用功能,animateAccordion(),它为你做动画:
var animateAccordion = function(elem, duration, easing) {
paragraphs.stop(true, true).slideUp(duration, easing);
$(elem).stop(true, true).slideDown(duration, easing);
}
您将保留这个函数,但有一点不同。因为您不再允许定制缓动,而是坚持使用默认设置,所以请移除该选项。您的插件循环现在应该如下所示:
return this.each(function() {
var $this = $(this);
var headings = $this.children(options.headings);
var paragraphs = $this.children(options.content);
var animateAccordion = function(elem, duration) {
paragraphs.stop(true, true).slideUp(duration);
$(elem).stop(true, true).slideDown(duration);
};
});
你的下一段代码做动画。它几乎不需要改变。目前是这样的:
paragraphs.not(":first").hide();
accordion.on("click", "h2", function() {
var t = $(this);
var tPara = t.next();
if(!tPara.is(":visible")) {
tPara.trigger("showParagraph");
}
});
accordion.on("showParagraph", "p", function() {
animateAccordion(this, 600, "easeInCirc");
});
只有几件需要修改。首先,你现在引用的不是变量accordion,而是$this。第二,两个调用都是通过"h2"或"p"引用标题和内容。你需要使用options.headings和options.content变量来代替。最后,从animateAccordion()参数中删除第三个参数,因为您不再支持不同的放松方法。
这使得代码看起来像这样:
paragraphs.not(":first").hide();
$this.on("click", options.headings, function() {
var t = $(this);
var tPara = t.next();
if(!tPara.is(":visible")) {
tPara.trigger("showParagraph");
}
});
$this.on("showParagraph", options.content, function() {
animateAccordion(this, 600);
});
你现在完成了。将它放入插件的循环中,您的手风琴就完成了!accordion.jquery.js现在应该是这样的:
(function($) {
$.fn.accordion = function(opts) {
var defaults = {
headings: "h2",
content: "p"
};
var options = $.extend({}, defaults, opts);
return this.each(function() {
var $this = $(this);
var headings = $this.children(options.headings);
var paragraphs = $this.children(options.content);
var animateAccordion = function(elem, duration) {
paragraphs.stop(true, true).slideUp(duration);
$(elem).stop(true, true).slideDown(duration);
};
paragraphs.not(":first").hide();
$this.on("click", options.headings, function() {
var t = $(this);
var tPara = t.next();
if(!tPara.is(":visible")) {
tPara.trigger("showParagraph");
}
});
$this.on("showParagraph", options.content, function() {
animateAccordion(this, 600);
});
});
};
})(jQuery);
如果你在浏览器中加载index.html,你应该会看到它像以前一样工作。祝贺您获得第一个“合适的”jQuery 插件!不过,你可以做些改进。首先是为回调提供支持。
添加回调支持
当你在第七章探索动画时,你发现了回调的美妙和运行一些代码的能力。如果您可以允许任何使用您的插件的人定义一个回调函数,该函数将在动画发生时运行,这意味着用户切换到了一个新的部分,这将是非常棒的。
您可能认为添加回调功能很复杂,但实际上并不复杂。首先要做的是给你的默认值添加一个新的选项,定义默认的回调函数应该是什么。如果用户没有定义一个函数,那么可以推测,一旦动画完成,她不想运行任何东西,所以默认应该是一个什么都不做的函数:
var defaults = {
headings: "h2",
content: "p",
callback: function() {}
};
接下来,您需要编辑您的animateAccordion()函数来接受回调。它应该把它作为一个参数,直接传递给动画。但是,您应该只将它传递给其中一个。如果你要把它同时传递给slideUp()和slideDown()函数,你会让它被调用两次。两个动画运行的时间相同,因此它们同时完成。这意味着您只能将其添加到一个动画中:
var animateAccordion = function(elem, duration, callback) {
paragraphs.stop(true, true).slideUp(duration);
$(elem).stop(true, true).slideDown(duration, callback);
};
最后,您需要编辑对animateAccordion()的调用以通过回调:
$this.on("showParagraph", options.content, function() {
animateAccordion(this, 600, options.callback);
});
注意options.callback只是对函数的引用。options.callback()会执行函数,这不是你想要的。引用一个没有括号的函数意味着你得到了一个对该函数的引用,而它并不执行。让我们来测试一下。编辑您的app.js文件,以便您传递一个回调:
$(function() {
$("#accordion").accordion({
headings: "h2",
content: "p",
callback: function() {
alert("changed");
}
});
});
现在,无论何时任何东西在手风琴里滑落,你都会收到警告。
您要做的最后一个调整是允许用户设置持续时间。对于任何包含动画的插件,让用户改变速度是很重要的。这很容易做到。添加默认值:
var defaults = {
headings: "h2",
content: "p",
callback: function() {},
duration: 600
};
同样,这只是编辑对animateAccordion()的调用:
$this.on("showParagraph", options.content, function() {
animateAccordion(this, options.duration, options.callback);
});
至此,您的 accordion 插件就完成了。下面是最终的代码:
(function($) {
$.fn.accordion = function(opts) {
var defaults = {
headings: "h2",
content: "p",
callback: function() {},
duration: 600
};
var options = $.extend({}, defaults, opts);
return this.each(function() {
var $this = $(this);
var headings = $this.children(options.headings);
var paragraphs = $this.children(options.content);
var animateAccordion = function(elem, duration, callback) {
paragraphs.stop(true, true).slideUp(duration);
$(elem).stop(true, true).slideDown(duration, callback);
};
paragraphs.not(":first").hide();
$this.on("click", options.headings, function() {
var t = $(this);
var tPara = t.next();
if(!tPara.is(":visible")) {
tPara.trigger("showParagraph");
}
});
$this.on("showParagraph", options.content, function() {
animateAccordion(this, options.duration, options.callback);
});
});
};
})(jQuery);
然后,您可以使用此选项来设置动画,如下所示:
$(function() {
$("#accordion").accordion({
duration: 10000
});
});
摘要
不要低估插件的力量。现在,您有了一个可以在任何项目中使用的简单手风琴。这是真正的好处:插件为您提供了模块化、可移植的代码,您可以根据需要获取和重用这些代码。在下一章中,您将增加复杂性并为 API 的使用编写一个插件。然后,我们将简要讨论如何分发您的插件,包括缩减您的源代码和编写好的文档。
十、更多 jQuery 插件
你以一个非常棒的 accordion 插件结束了上一章,这个插件是基于你在第五章中编写的初始 accordion 代码构建的。在这一章中,你将从第八章中取出你的 TVmaze API 工作,并把它变成一个插件。您将通过创建一个稍微不同类型的插件来实现这一点,这个插件直接存在于 jQuery 对象上。这意味着不像以前那样在集合中调用它:
$("div").somePlugin();
你直接称之为:
$.somePlugin();
在某些情况下,您应该使用第一种风格,而在其他情况下,您应该使用后者。本章解释了后者。本章还包括一些其他内容:
- 您将进一步探索使您的插件更有用的方法。保持它的通用性,并确保它完成它应该做的工作,使得其他人更容易使用它。您将看到实现这一点的技术。
- 我们将简要讨论为您的插件编写文档的最佳实践,并展示示例。
TVmaze API 插件
为了提醒您,下面是您编写的用于获取剧集列表的代码,包括我们在第七章中使用的 dataType 属性:
$(function() {
var req = $.ajax({
url: " http://api.tvmaze.com/shows/396/episodes ",
dataType: "jsonp"
});
req.done(function(data) {
console.log(data);
});
是时候把这段代码变成一个 jQuery 插件了。它将直接存在于 jQuery 对象上,因为您将扩展您的插件以拥有两个不同的方法。一个将获得单个节目的数据,另一个将获得该节目的所有剧集并构建一个列表。我们的目标是能够这样称呼它们:
$.tvmaze.getShow();
$.tvmaze.getEpisodes();
因此您的插件将存在于 jQuery 对象上,但是包含不止一个要调用的方法。
首先,为这个项目创建一个新目录。如果您记得以前使用过 npm,这里有一个再次使用它的好机会。如果您没有安装 http-server,请确保已经安装了 Node.js,然后在命令行键入npm install –g http-server。之后,同样使用命令行,键入http-server。这将使您的当前目录成为本地 web 服务器。
你可以通过输入localhost:8080在浏览器中看到你正在做的一切。
首先,您将创建页面的基本结构。为了让它看起来更好,您还将包括 Twitter Bootstrap ( http://getbootstrap.com )。创建空白的app.js和tvmaze.jquery.js文件,以及一个index.html文件,我们在这里添加需要使用 Bootstrap 来设计页面样式的部分(参考 http://getbootstrap.com 了解如何从 CDN 使用 Bootstrap 的说明):
<!DOCTYPE html>
<html>
<head>
<title>Chapter 10, TVMaze Plugin</title>
<link rel='stylesheet' href='add URL to bootstrap css'>
</head>
<body>
<div class="container">
<h1> TV Maze – TV Show Search </h1>
<form>
<div class="form-group">
<input type="text" class="form-control" id="showInput" placeholder="Search TV Show" required>
<button tyhpe="submit" class=" btn btn-primary" id-"submitButton">Submit</button>
</dir>
</form>
</div>
<div>
<table class="table">
<thead id="tableHead">
<tr>
<td>Name</td>
<td>Network</td>
<td>Type</td>
<td>Image</td>
</tr>
</thead>
<tbody id="tableBody">
</tbody>
</table>
<table class="table">
<thead>
<tr>
<td>Airdate</td>
<td>Episode Name</td>
<td>Episode Number</td>
<td>Season</td>
<td>Run Time</td>
<td>Summary</td>
</tr>
</thead>
<tbody id="episodeInfo">
</tbody>
</table>
</div>
</div>
<script src="jquery.js"></script>
<script src="tvmaze.jquery.js"></script>
<script src="app.js"></script>
</body>
</html>
此时,你的页面应该看起来如图 10-1 所示。
img/A310335_2_En_10_Fig1_HTML.jpg)
图 10-1。
The page with some basic CSS styling
打开tvmaze.jquery.js并添加以下内容。我们添加以下内容来创建 tvmaze 对象,并为其分配方法:
(function($) {
$.tvmaze = {
getEpisode: function(showId, callback) {
var req = $.ajax({
url:'http://api.tvmaze.com/shows/' + showId + '/episodes'
});
req.done(callBack);
},
getShow: function(showName, callBack) {
var req = $.ajax({
url:'http://api.tvmaze.com/search/shows/?q=' + showName
});
req.done(callBack);
}
};
})(jQuery);
现在插件已经就绪,您可以开始调用 TVmaze 了。随着时间的推移,您可以添加功能来进行其他 API 调用。现在,您可以打开app.js并开始让应用工作。类似于 TVmaze 插件,这将是一个 IIFE(立即调用的函数表达式)。简而言之,它将在创建后立即运行。
(function() {
$('#submitButton').on ('click', getShowName);
function getShowName(evt){
evt.proventDefault();
if($('#showInput')[0].valuie.length > 0){
let searchValue = $('#showInput')[0].value;
$().tvmaze.getShow(searchValue, (results) => {
displayShowResults(results[0].show);
});
}
}
})();
此时,该函数应该已经执行,jQuery 会向"submitButton"添加一个事件监听器。单击时,调用函数"getShowName()"并评估文本字段的值是否大于 0。如果是,它会将值赋给一个名为searchValue的变量。
在这里,您可以使用您最近创建的插件。使用从文本字段传递的值调用方法getShow()。在这个例子中,您可能会注意到回调函数看起来有点不同。这个例子使用了一个箭头函数。就您的目的而言,它的工作方式与普通的匿名函数完全相同。您的结果返回一个对象数组,然后传递给displayShowResults()函数,您接下来添加它,如下所示:
Note
有关箭头函数与函数表达式有何不同的更多信息,请查看 MDN Web 文档: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
function displayShowResults(results){
$('#tableBody').html('<td id="episodeName" data-episodeid="' + results.id + '">'
+ result.name + '</td>' + '<td>'
+ checkNetwork(results.network) + '</td>'
+ '<td>' + results.type + '</td>'
+ '<td> <img class="image-border" src=" '
+ results.image.medium '"> </td>');
$('#episodeInfo tr').remove();
$('#episodeName').on('click', (event) => {
getEpisodes(event.target.dataset.episodeid);
});
}
function checkNetwork(networkName)
if(networkName != null) {
return networkName.name;
}else{
return 'Not Listed';
}
}
在这里,您开始显示您的插件为您进行的 API 调用的结果。jQuery 添加一行结果,显示 ID、名称和图像。还有一个叫做checkNetwork()的函数,只有在 API 返回的结果为 null 的情况下才会包含。在这种情况下,您会得到一个错误。为了避免这种情况,您可以检查是否有空值,如果有,就返回字符串'Not Listed'。
下一行删除任何以前的电视节目列表。第一次运行时,不会删除任何内容,但是您很快就会看到这一点变得多么重要。另一个点击事件被添加到 ID episodeName中。
如果您想知道数据属性在做什么,这里是它的用武之地。data 属性是 HTML5 的一部分,允许您添加没有任何可视化表示的额外信息。检索信息时,可以查看dataset属性,调用自己定制的属性episodeid。这样,您现在可以再次调用 TVmaze 插件并获取该电视节目的所有剧集:
function getEpisodes(episodeID){
$().tvmaze.getEpisodes(episodeID, function(results){
for(var I = 0; I < results.length; i++){
$('#episodeInfo').append('<tr> <td>’
+ results[i].airdate + '</td> <td>'
+ results[i].name + '</td> <td>'
+ results[i].number + '</td> <td>'
+ results[i].season + '</td> <td>'
+ results[i].runtime + '</td> <td>'
+ results[i].summary + '<td>'
+ '</tr>')
}
});
}
本例中的最后一个函数调用 TVmaze 插件。当结果返回时,它将遍历这些结果,并在现有的表中添加一个新行,其中包含有关剧集的信息。
如果您在本地 web 服务器(例如,http-server)上运行它,您应该能够调用 API 并获得结果。图 10-2 显示了应该是什么样子的部分截图。
img/A310335_2_En_10_Fig2_HTML.jpg)
图 10-2。
Results from the TVmaze site using the jQuery plugin
文件
我们将非常简要地讨论如何记录您的插件。为您的插件编写文档对其他可能使用它的开发人员来说很重要,但对您来说也是如此。当您在一段时间后重新访问一个插件时,好的文档确实很有帮助。
您需要清楚地记录每个方法。最好提供以下详细信息:
- 用一两句话描述这个方法的作用。
- 请注意该方法接受的选项、示例值以及每个选项的默认值。
- 举例说明插件的用法及其返回的数据。
以下是我如何记录你的getShow()方法的例子:
The Getshow Method
getShow()是一个方法,它接受一个表示电视节目名称的字符串,并向 TVmaze 发出 Ajax 请求,返回该节目的 JSON 数据。
showName:这是一个字符串,表示您想要获取其数据的电视节目的 ID;比如:“重力下降”callback:Ajax 请求返回数据时将被调用的函数。
它只需要一个字符串:
下面是一个使用示例,以及收到的响应:
$.tvmaze.getShow({
showName:"Star Wars: Rebels",
callback: function(data) {
console.log(data);
}
});
返回的 JSON 是:
img/A310335_2_En_10_Fig3_HTML.jpg)
这是我如何记录方法的一个例子。我的主要原则是,如果我在考虑是否要记录某个功能,我无论如何都会去做。文档太多总比不够好。
摘要
这是又一个复杂的章节,您在其中探索了组织代码并学习了如何有效地重构。您的插件的最终用户将调用的方法都只有两行长,主要工作在您的工具方法中完成。
接下来,您将通过创建一个图像滑块插件来完成这本书。您将使用到目前为止在本书中学到的所有内容的组合来制作一个包装良好的 jQuery 插件,该插件具有良好的结构,通过选项为用户提供定制,并且可以很容易地重用。
十一、jQuery 图像滑块
您将通过构建著名的 jQuery 插件:image slider 来完成这本书。这将把你到目前为止只孤立研究过的书中的许多部分汇集在一起。您将使用动画来制作图像和事件的动画,让用户点击滑动条,最终得到一个可以投入生产的插件。你还会遇到你还没有研究过的新功能。例如,你可以把你的滑块挂在键盘上,这样用户就可以按向左或向右的箭头来导航。而且,通过允许用户暂停和播放滑块,以及让它每 20 秒自动播放一次动画,您将进一步增加复杂性。尽管你在第七章中制作了一个滑块,你将从头开始这个新的。
行动(或活动、袭击)计划
在着手一个大项目之前,不管它是一个完整的网站还是一个插件,你都应该列出你想要实现的关键特性。这是你在本章要遵循的列表:
- 允许用户点击屏幕上的前进和后退按钮来浏览图像。
- 让用户使用左右箭头键浏览图像。
- 当到达滑块末尾时,使导航按钮循环到开头。
- 让幻灯片每 20 秒自动播放一次。
- 如果用户单击“下一步”或“上一步”按钮,重置滑块计时器,这样它就不会在用户单击按钮后自动滚动。
我们开始吧!
项目设置
您已经这样做了很多次,可能已经习惯了,但是您需要用适当的子文件夹创建一个新的项目目录,其中应该包含以下文件(暂时将它们全部留空):
index.htmlcss/style.cssscripts/app.jsscripts/slider.jquery.js
你还应该下载最新版本的 jQuery 并保存到jquery.js。如果你已经安装了 Node.js,那么确保你已经安装了 http-server。
编辑index.html的内容,看起来像下面的代码。这只是加载您的 JavaScript 和 CSS 文件,并设置将成为滑块的 HTML 结构。
<!DOCTYPE html>
<html>
<head>
<title>Chapter 11, Image Slider</title>
<link rel="stylesheet" href="css/style.css">
<script src="scripts/jquery.js"></script>
<script src="scripts/slider.jquery.js"></script>
<script src="scripts/app.js"></script>
</head>
<body>
<div class="slider">
<ul>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
<li><img src=" http://placekitten.com/g/300/300 " alt="Kitten" width="300" height="300" /></li>
</ul>
<a href="#" class="button back">Back</a>
<a href="#" class="button forward">Forward</a>
</div>
</body>
</html>
你将要使用的 CSS 和你在第七章中用于滑块的 CSS 几乎是一样的。它只是让无序列表大到足以水平容纳所有图片,然后让它所在的div的宽度仅够显示一张图片。这里我们使用的图片来自 http://unsplash.com 。
body {
padding: 50px;
}
.slider {
width: 300px;
overflow: hidden;
height: 400px;
}
.slider ul {
list-style: none;
width: 3000px;
height: 300px;
margin: 0;
padding: 0;
}
.slider li {
float: left;
width: 300px;
height: 300px;
}
.button {
font-family: Arial, sans-serif;
font-size: 14px;
display: block;
padding: 6px;
border: 1px solid #ccc;
margin: 10px 0 0 0;
}
.back {
float: left;
}
.forward {
float: right;
}
a:link, a:visited {
color: blue;
text-decoration: underline;
}
a:hover {
text-decoration: none;
}
HTML 和 CSS 就绪后,在命令行运行http-server,你应该会看到类似图 11-1 的东西。
img/A310335_2_En_11_Fig1_HTML.jpg)
图 11-1。
The styled slider, complete with kittens
插件设置
现在在你选择的文本编辑器中打开slider.jquery.js。添加下面的代码,它只对您的插件进行初始设置。它通过用用户传入的设置扩展默认设置来建立settings变量(现在这是空的,但是随着您的继续,您会发现应该将变量转换为选项的地方)。然后它进入循环,该循环遍历调用插件的每个元素,并在其中设置一些初始变量。
function($) {
$.fn.slider = function(options) {
var defaults = {};
var settings = $.extend({}, defaults, options);
return this.each(function() {
var $slider = $(this);
var $sliderList = $slider.children("ul");
var $sliderItems = $sliderList.children("li");
var $allButtons = $slider.find(".button");
var $buttons = {
forward: $allButtons.filter(".forward"),
back: $allButtons.filter(".back")
};
});
};
})(jQuery);
请注意,这段代码还将这两个按钮存储在一个对象中。这意味着您可以使用$buttons.forward按钮前进,使用$buttons.back按钮后退。将它们作为$buttons.forward放在一个对象中比每个都有一个变量更好读。要访问它们,您可以使用filter()方法缩小按钮集,只包含您需要的具有特定类的按钮。
接下来,进入您的app.js文件并添加以下内容:
$(function() {
$(".slider").slider();
});
现在您已经准备好开始了。
制作滑块动画
你在第七章中制作的滑块有一个用于制作滑块动画的实用函数。它有三个参数:方向、持续时间和回调。虽然你不会直接从章节 7 滑块中复制每一段代码,但是你会使用工具方法:
var animateSlider = function(direction, duration, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=300px"
}, duration, callback);
};
注意一个小的调整:变量现在被称为$sliderList,而不是简单的sliderList。
Note
你会注意到,现在我们使用的是混合变量,一些变量以$开头($sliderList),一些没有(settings)。处理大量变量的一种方法是在开头给所有引用 jQuery 对象的变量一个$——以便区分它们。
将该方法添加到设置$buttons对象的行的正下方。现在是时候给按钮添加一个click事件来让滑块工作了。你可以用你在第七章中的同样方法来做这件事。当点击一个按钮时,检查它是否是后退按钮。如果是,以"+"为方向调用animateSlider(),否则以"-"调用。这个方法现在看起来很简单,事实也的确如此。稍后它将需要重构。
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
animateSlider((isBackButton ? "+" : "-"), 1000);
event.preventDefault();
});
滑块在这一点上工作,但有一些大的陷阱。您可以无限地点击按钮,这意味着您可以滚动过去的最后一个图像,并在一个空白页结束。你会记得你在第七章中处理过这个问题,当滑块到达第一张/最后一张图片时,你简单地禁用了后退/前进按钮。这一次,你将会以不同的方式处理它,并使滑块连续循环,这样当你在最后一个图像时点击前进按钮将会把你带回到第一个图像。
无限循环
如果margin-left是 0,你知道滑块在开始,如果它的左边距是-( (numberOfImages -1 ) * widthOfOneImage),你知道滑块在结尾。以下是检测滑块是在开头还是结尾的两个小方法:
var isAtBeginning = function() {
return parseInt($sliderList.css("margin-left"), 10) === 0;
};
var isAtEnd = function() {
var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();
return parseInt($sliderList.css("margin-left"), 10) === -endMargin;
};
记住,$sliderList.css("margin-left")会给你一个字符串——比如“300px ”,所以使用 JavaScript 的parseInt()来从中解析整数 300。parseInt()采用第二个参数,这是使用的基础。在这里,你可以发现你可能在这个滑块中不止一次地解析页边空白,所以把它变成一个新的实用方法。这整理了代码:
var getLeftMargin = function() {
return parseInt($sliderList.css("margin-left"), 10);
};
var isAtBeginning = function() {
return getLeftMargin() === 0;
};
var isAtEnd = function() {
var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();
return getLeftMargin() === -endMargin;
};
编写一个额外的方法看起来会让你的代码更长,但是它极大地整理了前面的例子;另外,你很可能需要在其他地方使用它。
既然您可以检测滑块是在开头还是结尾,那么您需要对无限循环进行排序。
如果滑块在开头,点击了后退按钮,就需要一直走到结尾。如果滑块在末尾,并且单击了前进按钮,则需要一直跳到开头。前进按钮的行为稍微容易一些,因为将滑块返回到起点只是将左边距设置为 0。
您可以在所单击按钮的事件处理程序中完成此操作。如果单击的按钮不是后退按钮,并且您在末尾,那么您需要循环:
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
// loop to the beginning
}
animateSlider((isBackButton ? "+" : "-"), 1000); event.preventDefault();
});
要循环,您需要将滑块的边距设置为 0。因为您可能需要多次这样做,所以创建另一个实用方法,并将其插入到您定义animateSlider()方法的位置的正下方:
var animateSliderToMargin = function(margin, duration, callback) {
$sliderList.stop(true, true).animate({
"margin-left": margin
}, duration, callback);
};
接下来你要制作动画,因为动画会让用户更清楚地看到发生了什么。下面的代码显示了如何在 click 处理程序中使用新方法。一旦这个改变被实现,你将能够无限循环地向前移动滑块。
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0, 1000);
} else {
animateSlider((isBackButton ? "+" : "-"), 1000);
}
event.preventDefault();
});
接下来,您将使后退按钮工作。为此,您需要将边距设置为尽可能大的负边距。您在编写isAtEnd()方法时已经计算过了:
var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();
因为您将再次使用它,所以您需要将它移到一个实用方法中,这样就不会重复。然而,在一个工具方法中使用它是多余的。当滑块初始化时,你可以简单地计算这个变量一次,然后在以后引用它。就在您定义变量$buttons的下方,添加以下内容:
var endMargin = ($sliderItems.length - 1) * $sliderItems.first().children("img").width();
现在更新isAtEnd()方法,简单地使用:
var isAtEnd = function() {
return getLeftMargin() === -endMargin;
};
你要再做一个改变。与其保持endMargin为正值,并在需要时将其用作-endMargin,不如一开始就简单地将endMargin设为负值要容易得多。将endMargin的变量声明改为如下:
var endMargin = -(($sliderItems.length - 1) * $sliderItems.first().children("img").width());
现在你的isAtEnd()方法更简单了:
var isAtEnd = function() {
return getLeftMargin() === endMargin;
};
现在,您可以在事件处理程序中使用它来使滑块在后退时无限循环,如下所示:
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0, 1000);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin, 1000);
} else {
animateSlider((isBackButton ? "+" : "-"), 1000);
}
event.preventDefault();
});
首先,检查前进按钮,确认用户位于滑块上的最后一个图像。如果是这样,动画回到开始。如果没有,检查是否单击了后退按钮,用户是否在滑块的开始位置。如果是,动画回到结尾;否则,你只是像平常一样前后移动,因为用户既不在开始也不在结束。
如果你在浏览器中刷新index.html,你应该可以点击后退按钮,转到列表的最末尾。事实上,你现在应该可以随意点击后退和前进,而不会到达“终点”,因为一旦到达终点,你的滑块就会循环。
赶上
这是一个休息一下,看看我们在哪里的好地方。下面的代码显示了我们的slider.jquery.js文件的全部内容:
(function($) {
$.fn.slider = function(options) {
var defaults = {};
var settings = $.extend({}, defaults, options);
return this.each(function() {
// store some initial variables
var $slider = $(this);
var $sliderList = $slider.children("ul");
var $sliderItems = $sliderList.children("li");
var $allButtons = $slider.find(".button");
var $buttons = {
forward: $allButtons.filter(".forward"),
back: $allButtons.filter(".back")
};
var endMargin = -(($sliderItems.length - 1) * $sliderItems.first().children("img").width());
var animateSlider = function(direction, duration, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=300px"
}, duration, callback);
};
var animateSliderToMargin = function(margin, duration, callback) {
$sliderList.stop(true, true).animate({
"margin-left": margin
}, duration, callback);
};
var getLeftMargin = function() {
return parseInt($sliderList.css("margin-left"), 10);
};
var isAtBeginning = function() {
return getLeftMargin() === 0;
};
var isAtEnd = function() {
return getLeftMargin() === endMargin;
};
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0, 1000);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin, 1000);
} else {
animateSlider((isBackButton ? "+" : "-"), 1000);
}
event.preventDefault();
});
});
};
})(jQuery);
也看看你可以把一些东西变成用户可以传入的选项。显而易见的是每个动画的持续时间。您在多个地方将持续时间手动编码为 1000,因此将其设置为选项将减少代码的重复性。
编辑defaults变量的声明,为持续时间设置默认值:
var defaults = {
duration: 1000
};
您需要改变两种方法。首先是animateSlider()法。您将持续时间和回调传递到这个方法中,但是现在您只需要传递方向和回调。将您的animateSlider()方法改为如下所示:
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=300px"
}, settings.duration, callback);
};
最后,在按钮点击事件处理程序中编辑对animateSlider()的调用,以便只传递方向,而不传递持续时间。你可以通过一个回调,但现在,你不需要,所以不要打扰。
animateSlider((isBackButton ? "+" : "-"));
接下来,更新animateSliderToMargin:
var animateSliderToMargin = function(margin, callback) {
$sliderList.stop(true, true).animate({
"margin-left": margin
}, settings.duration, callback);
};
同样,在事件处理程序中更新对它的调用,使它们不再通过持续时间:
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin);
} else {
animateSlider((isBackButton ? "+" : "-"));
}
event.preventDefault();
});
在开发插件时,您还应该注意固定值。你所有的价值都需要计算;例如,滑块中每个图像的宽度应该是计算出来的,而不是硬编码的。虽然您已经用这个插件完成了,但是有一个地方我们保留了一个硬编码的值,应该在那里进行计算。你能看到哪里吗?
就在你的animateSlider()法里:
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=300px"
}, settings.duration, callback);
};
每次以固定的 300 像素制作动画。你应该从一张图片的宽度来计算。您可以在animateSlider()方法中这样做,但是您应该在插件设置好之后,在最顶端进行计算。如果你用animateSlider()方法来做,它会在每次动画运行时重新计算,效率很低。在计算endMargin变量的代码行上方添加以下代码行:
var imageWidth = $sliderItems.first().children("img").width();
然后,您可以整理endMargin变量,使用刚刚计算的imageWidth变量:
var endMargin = -(($sliderItems.length - 1) * imageWidth);
现在在animateSlider()方法中使用这个变量:
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, callback);
};
当您传入宽度时,也可以从动画调用中删除“px”——如果您不指定,jQuery 默认为像素。
保持跟踪
在添加键盘支持之前,我们需要实现一个小特性。最好在滑块下方显示一个与滑块中当前图像相关的数字,这样当您第一次加载页面时,它显示 1,当您单击前进时,它显示 2,以此类推。在阅读如何实现之前,先看看你自己能不能做到。你需要
- 有一个变量来跟踪当前图像。
- 每次用户后退或前进时更新此变量。
- 支持无限循环。例如,如果用户在第一张图片上点击后退按钮,数字不应该从 1 到 0,而应该从 1 到最后一张图片,也就是 9。
- 在 HTML 中添加一个元素来显示数字,并在每次索引改变时更新这个值。
这是我们的解决方案,但尝试自己做。首先,创建新变量:
var totalImages = $sliderItems.length;
var currentIndex = 1;
在 HTML 中,在 Forward 链接下面,添加一个快速的span来显示值:
<span class="index">1</span>
然后,在指定currentIndex的位置下添加另一个变量,以存储对这个新 span 元素的引用:
var $index = $(".index");
将index类添加到样式化按钮的同一组 CSS 中,并将文本居中对齐。将这些样式添加到您的style.css文件的底部:
.button, .index {
font-family: Arial, sans-serif;
font-size: 14px;
display: block;
padding: 6px;
border: 1px solid #ccc;
margin: 10px 0 0 0;
}
.index {
text-align: center;
}
这使得滑块看起来如图 11-2 所示。
img/A310335_2_En_11_Fig2_HTML.jpg)
图 11-2。
The newly styled span tag showing the index
接下来,在您的slider.jquery.js文件中创建一个名为updateIndex()的方法,它接收新的索引。然后,它将这个值存储在currentIndex变量中,并使用您创建的$index变量更新 span 中显示的值。
var updateIndex = function(newIndex) {
currentIndex = newIndex;
$index.text(currentIndex);
};
最后,只是在 click 事件处理程序中使用这个方法的问题。下面的代码显示了如何做到这一点。调用它并传入 0 或最后一个数字(如果滑块从末尾循环到开头)。
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0);
updateIndex(1);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin);
updateIndex(totalImages);
} else {
animateSlider((isBackButton ? "+" : "-"));
}
event.preventDefault();
});
接下来,您有显示新的animateSlider()方法的代码。根据动画的前进方向,只需从索引中减去 1 或加上 1。
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, callback);
if(direction == "+") {
// back button
updateIndex(currentIndex - 1);
} else {
// forward
updateIndex(currentIndex + 1);
}
};
当然,您可以使用三元运算符对其进行重构。这个方法现在被很好地精简了:
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, callback);
var increment = (direction === "+" ? -1 : 1);
updateIndex(currentIndex + increment);
};
再一次,这里是我的整个slider.jquery.js文件,这样您可以很容易地将您的索引实现与我们的进行比较,并确保在您开始添加键盘支持之前,您与我们在同一页上:
(function($) {
$.fn.slider = function(options) {
var defaults = {
duration: 1000
};
var settings = $.extend({}, defaults, options);
return this.each(function() {
// store some initial variables
var $slider = $(this);
var $sliderList = $slider.children("ul");
var $sliderItems = $sliderList.children("li");
var $allButtons = $slider.find(".button");
var $buttons = {
forward: $allButtons.filter(".forward"),
back: $allButtons.filter(".back")
};
var $index = $(".index");
var totalImages = $sliderItems.length;
var imageWidth = $sliderItems.first().children("img").width();
var endMargin = -(totalImages - 1) * imageWidth);
var currentIndex = 1;
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, callback);
var increment = (direction === "+" ? -1 : 1);
updateIndex(currentIndex + increment);
};
var animateSliderToMargin = function(margin, callback) {
$sliderList.stop(true, true).animate({
"margin-left": margin
}, settings.duration, callback);
};
var getLeftMargin = function() {
return parseInt($sliderList.css("margin-left"), 10);
};
var isAtBeginning = function() {
return getLeftMargin() === 0;
};
var isAtEnd = function() {
return getLeftMargin() === endMargin;
};
var updateIndex = function(newIndex) {
currentIndex = newIndex;
$index.text(currentIndex);
};
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0);
updateIndex(1);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin);
updateIndex(totalImages);
} else {
animateSlider((isBackButton ? "+" : "-"));
}
event.preventDefault();
});
});
};
})(jQuery);
键盘支持
添加键盘支持是我们故意在第 5 和 6 章的事件报道中省略的事情之一,所以我们可以在这里讨论。这实际上比你想象的要简单得多。
其中一个事件是keyup事件。当一个键被按下然后释放时,该事件被激发。你所需要做的就是捕捉那个事件,并在它发生时做些什么。记住,对于每个事件,jQuery 都要通过事件对象;该事件对象的属性之一是keyCode。它对应于触发事件的按键。每个密钥都有一个唯一的数字,它是一个整数。您将使用的两个键是左右箭头键。左箭头的键码是 37,右箭头的键码是 39。如果你想找到其他键的键码,Cambia Research 在 https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes 的博客文章提供了一个全面的列表。
您可以监听keyup事件,并根据键码触发后退或前进按钮上的点击事件。为此,您需要一个元素来绑定keyup事件。它需要是一个始终在焦点上的元素,这样当用户按下箭头时,无论鼠标位于何处,滑块都将工作。
你的第一个想法可能是使用“body”,这是明智的。然而,旧版本的 Internet Explorer 在这方面的支持有点粗糙。在我们对这本书的研究中,我们发现“body”在 IE9 及以下版本上不起作用。实际上最好使用document.documentElement,它是文档对象的一个属性——一个存储文档内容信息的 DOM 对象。documentElement包含对页面上所有内容的引用,因为它返回的元素是根元素——浏览器中的<html>标签。了解了这一点,您可以将一个keyup事件绑定到它,并根据所按下的键触发一次点击:
$(document.documentElement).on("keyup", function(event) {
if(event.keyCode === 37) {
//left arrow
$(".back").trigger("click");
} else if (event.keyCode === 39) {
//right arrow
$(".forward").trigger("click");
}
});
如果你在浏览器中打开你的滑块,你会发现你现在可以用箭头来控制你的滑块了!就这么简单。
你的下一个挑战是每 20 秒自动激活滑块。为此,您需要能够通过直接调用方法来激活滑块,而不仅仅是通过触发按钮上的 click 事件。这是因为当自动触发动画时,与用户手动点击按钮相比,你需要做不同的事情。因此,您将获取事件处理程序中包含的主要功能,并将其移动到自己的函数中。
调用这个函数triggerSlider(),将事件处理程序的内容移入其中:
var triggerSlider = function(direction, callback) {
var isBackButton = (direction === "+");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0, callback);
updateIndex(1);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin, callback);
updateIndex(totalImages);
} else {
animateSlider(direction, callback);
}
};
这个函数将接受一个参数,方向,它可以是"+"或"-"。然后在此基础上设置isBackButton的值。JavaScript 将评估(direction === "+")为true或false,并相应地设置isBackButton的结果。这意味着按钮的 click 事件处理程序要小得多:
$allButtons.on("click", function(event) {
var isBackButton = $(this).hasClass("back");
triggerSlider((isBackButton? "+" : "-"));
event.preventDefault();
});
您需要修改keyup事件处理程序来调用triggerSlider():
$(document.documentElement).on("keyup", function(event) {
if(event.keyCode === 37) {
triggerSlider("+");
} else if (event.keyCode === 39) {
triggerSlider("-");
}
});
这给你留下了更好的代码和一种触发动画而不触发点击事件的方法。这是很重要的,因为你会看到下一步,当你研究自动动画你的滑块。
自动动画
为了实现滑块的自动动画,您将使用一个名为setTimeout()的 JavaScript 方法。它有两个参数:一个函数和一个以毫秒为单位的时间。您传入的函数在特定时间后执行;例如:
setTimeout(function() { alert("hey"); }, 1000);
如果你运行这个,你会看到警告弹出,但只是在 1 秒钟后。你可以用它来制作滑块的动画。
为了让你的滑块无限运行,你可以创建一个执行然后调用setTimeout()的函数,把它自己作为第一个参数传递。下面的代码演示了这一点,但是您不应该在浏览器中执行它!你会收到无数的警告。
var alertHey = function() {
alert("Hey");
setTimeout(alertHey, 1000);
}
setTimeout(alertHey, 1000);
alertHey函数提醒“嘿”,然后运行setTimeout(), 1 秒钟后调用。一旦你调用了这个函数,它将继续每秒运行一次。
知道了这一点,你就可以很容易地实现你的自动滑动:
var automaticSlide = function() {
setTimeout(function() {
triggerSlider("-", function() {
automaticSlide();
});
}, 1000);
};
setTimeout(automaticSlide, 1000);
如果你在浏览器中刷新,你应该看到你的滑块每秒钟都在动。但是有个问题!你可以点击,但这不会停止动画。向后导航特别困难,因为滑块每秒向前移动一次。
在解决这个问题之前,您将添加一个新选项animationDelay,它是自动动画之间的间隔。这里,默认值设置为 5000,比之前的值稍高。如果你想多一点或少一点,请随意调整。
var defaults = {
duration: 1000,
animationDelay: 5000
};
然后更新动画代码:
var automaticSlide = function() {
setTimeout(function() {
triggerSlider("-", function() {
automaticSlide();
});
}, settings.animationDelay);
};
setTimeout(automaticSlide, settings.animationDelay);
有可能清除正在等待的超时。setTimeout()返回一个 ID,即挂起超时的 ID。然后您可以将它传递给clearTimeout()来取消超时。因此,您需要执行以下操作:
- 当用户单击按钮时,取消超时。
- 设置另一个超时时间,但要长得多(可能 30 秒),在这个时间重新启动自动动画。
- 如果用户同时点击按钮,也取消超时。
首先,插入新的一行,设置初始超时,将其结果存储在变量timer中:
var timer = setTimeout(automaticSlide, settings.animationDelay);
然后,编辑automaticSlide()方法以使用相同的变量:
var automaticSlide = function() {
timer = setTimeout(function() {
triggerSlider("-", function() {
automaticSlide();
});
}, settings.animationDelay);
};
现在您在timer变量中有了一个对当前设置的定时器的引用,您可以通过将它传递给clearTimeout()来取消它。为此,创建另一个名为resetTimer()的实用方法。这应该会取消挂起的超时,然后设置一个新的超时,但时间周期要长得多:
var resetTimer = function() {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(automaticSlide, 30000);
}
该方法首先检查timer变量是否计算为true,这意味着它包含一个值。如果是这样,就清除超时。然后设置一个新的超时,将结果存储回timer变量。然后,您需要调用此方法两次:第一次,当用户单击按钮时:
$allButtons.on("click", function(event) {
resetTimer();
var isBackButton = $(this).hasClass("back");
triggerSlider((isBackButton? "+" : "-"));
event.preventDefault();
});
当用户使用箭头键导航时:
$(document.documentElement).on("keyup", function(event) {
if(event.keyCode === 37) {
resetTimer();
triggerSlider("+");
} else if (event.keyCode === 39) {
resetTimer();
triggerSlider("-");
}
});
有了这些改变,你应该能够使用箭头键或按钮来导航,而不会有自动滑动的阻碍。如果你等 30 秒,自动滑行应该会恢复。
错误修复
对于任何像这样的大型插件,总是会有 bug 出现。我们特意留了一个来演示一个实际的 bug,并告诉你如何解决它。尝试在浏览器中打开滑块,然后快速按下键盘上的左箭头按钮。如果你这样做的次数足够多,你应该会得到如图 11-3 所示的结果。您已经设法跳过了无限循环检测代码,并在 Image–9 上结束。
img/A310335_2_En_11_Fig3_HTML.jpg)
图 11-3。
Definitely a bug that needs fixing!
花点时间想想可能会发生什么。其实一点都不太明显。作为线索,这个 bug 存在于以下两种方法中:
var isAtBeginning = function() {
return getLeftMargin() === 0;
};
var isAtEnd = function() {
return getLeftMargin() === endMargin;
};
当你连续点击左箭头时,它会在很短的时间内发出大量的动画。虽然您使用 jQuery 的stop()方法来消除这个问题,但是您可能会在滑块的开始处结束,但是边距不完全是 0。当许多动画快速启动,然后突然停止时,你可能会在两个图像之间的空白处结束。然后,边距可能不是您期望的整数(0、300、600 等)。),反而是略偏。所以你需要不那么具体。如果滑块在开头,左边距将等于或大于 0。同样,如果滑块在末尾,左边距将小于或等于endMargin(小于是因为值是负数,记住)。继续前进,做出改变:
var isAtBeginning = function() {
return getLeftMargin() >= 0;
};
var isAtEnd = function() {
return getLeftMargin() <= endMargin;
};
当您现在运行滑块并快速按下箭头键时,您会发现无法跳过开头或结尾。不过,还有一个问题:显示的指数可能会短暂地过高或过低。你有图像 1-9,但是当你太快地点击左箭头时,索引将短暂地到达 0 或 10。你能找出这个错误的来源吗?它在animateSlider()方法中:
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, callback);
var increment = (direction === "+" ? -1 : 1);
updateIndex(currentIndex + increment);
};
在这里,您在动画开始后立即更新当前索引。相反,你应该在动画结束后更新当前的索引,所以你应该在回调中完成。通过这样做,您可以确保动画完成后,索引会按照您的预期进行更新。这也意味着只有当动画运行到结束时,索引才会更新。您需要重构您的animateSlider()方法,因此在回调中更新索引。一旦这样做了,就可以调用传入到animateSlider()方法中的回调。这显示了您重构的animateSlider()方法:
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, function() {
var increment = (direction === "+" ? -1 : 1);
updateIndex(currentIndex + increment);
if(callback && typeof callback == "function") {
callback();
}
});
};
在回调中,您可以更新索引,然后调用传入的回调。这里要小心,因为你首先需要检查回调是否被设置为某个值,并且它是一个函数。快速条件句会帮你做到这一点。有了这个,你的滑球有了很大的改进,错误也被粉碎了。干得好!
摘要
多么精彩的一章!您从头开始创建了自己的图像滑块,包括自动动画和键盘快捷键。通过适当地清除动画并以数学方式计算所有值,而不是硬编码它们中的任何一个,您已经使它变得健壮和通用。
为了让你欣赏自己的作品,下面是完整的滑块插件:
(function($) {
$.fn.slider = function(options) {
var defaults = {
duration: 1000,
animationDelay: 5000
};
var settings = $.extend({}, defaults, options);
return this.each(function() {
// store some initial variables
var $slider = $(this);
var $sliderList = $slider.children("ul");
var $sliderItems = $sliderList.children("li");
var $allButtons = $slider.find(".button");
var $buttons = {
forward: $allButtons.filter(".forward"),
back: $allButtons.filter(".back")
};
var $index = $(".index");
var imageWidth = $sliderItems.first().children("img").width();
var endMargin = -(($sliderItems.length - 1) * imageWidth);
var totalImages = $sliderItems.length;
var currentIndex = 1;
var isPaused = false;
var animateSlider = function(direction, callback) {
$sliderList.stop(true, true).animate({
"margin-left" : direction + "=" + imageWidth
}, settings.duration, function() {
var increment = (direction === "+" ? -1 : 1);
updateIndex(currentIndex + increment);
if(callback && typeof callback == "function") {
callback();
}
});
};
var animateSliderToMargin = function(margin, callback) {
$sliderList.stop(true, true).animate({
"margin-left": margin
}, settings.duration, callback);
};
var getLeftMargin = function() {
return parseInt($sliderList.css("margin-left"), 10);
};
var isAtBeginning = function() {
return getLeftMargin() >= 0;
};
var isAtEnd = function() {
return getLeftMargin() <= endMargin;
};
var updateIndex = function(newIndex) {
currentIndex = newIndex;
$index.text(currentIndex);
};
var triggerSlider = function(direction, callback) {
var isBackButton = (direction === "+");
if(!isBackButton && isAtEnd()) {
animateSliderToMargin(0, callback);
updateIndex(1);
} else if(isBackButton && isAtBeginning()) {
animateSliderToMargin(endMargin, callback);
updateIndex(totalImages);
} else {
animateSlider(direction, callback);
}
};
var automaticSlide = function() {
timer = setTimeout(function() {
triggerSlider("-", function() {
automaticSlide();
});
}, settings.animationDelay);
};
var timer = setTimeout(automaticSlide, settings.animationDelay);
var resetTimer = function() {
if(timer) {
clearTimeout(timer);
}
timer = setTimeout(automaticSlide, 30000);
}
$allButtons.on("click", function(event) {
resetTimer();
var isBackButton = $(this).hasClass("back");
triggerSlider((isBackButton? "+" : "-"));
event.preventDefault();
});
$(document.documentElement).on("keyup", function(event) {
if(event.keyCode === 37) {
resetTimer();
triggerSlider("+");
} else if (event.keyCode === 39) {
resetTimer();
triggerSlider("-");
}
});
});
}
})(jQuery);
本章和其他章节的所有代码都可以从 Apress 网站下载。
结论
这就把我们带到了这本书的结尾。我们希望您读完它后,能够自信地利用 JavaScript 和 jQuery 来解决您面临的任何相关问题。除了展示实际用途之外,我们还试图展示重要的概念和方法,希望这些概念和方法能够帮助您解决问题并产生一个健壮的解决方案。如果您想知道接下来该怎么做,我们建议您坚持使用从本书中学到的 jQuery 技能,如果您想更深入地研究 JavaScript,我们建议您阅读一本关于纯 JavaScript 的书。jQuery 只是 JavaScript,JavaScript 越好,jQuery 越好。