jQuery3 学习手册(二)
原文:
zh.annas-archive.org/md5/B3EDC852976B517A1E8ECB0D0B64863C译者:飞龙
第五章:操纵 DOM
Web 经验是 Web 服务器和 Web 浏览器之间的合作伙伴关系。传统上,生成可供浏览器使用的 HTML 文档一直是服务器的职责。我们在本书中看到的技术略微改变了这种安排,使用 CSS 技术实时改变 HTML 文档的外观。但要真正发挥我们的 JavaScript 实力,你需要学会修改文档本身。
在本章中,我们将涵盖:
-
使用文档对象模型(DOM)提供的接口修改文档
-
在页面上创建元素和文本
-
移动或删除元素
-
通过添加、删除或修改属性和属性,转换文档
操纵属性和属性
在本书的前四章中,我们一直在使用.addClass()和.removeClass()方法来演示如何在页面上更改元素的外观。尽管我们非正式地讨论了这些方法,提到了操纵class属性,但 jQuery 实际上修改了一个名为className的 DOM 属性。.addClass()方法创建或添加到该属性,而.removeClass()删除或缩短它。再加上.toggleClass()方法,它在添加和删除类名之间切换,我们就有了一种高效而健壮的处理类的方式。这些方法特别有帮助,因为它们在元素上添加类时避免了添加已经存在的类(所以我们不会得到<div class="first first">,例如),并且正确处理应用于单个元素的多个类的情况,例如<div class="first second">。
非类属性
我们可能需要不时访问或更改其他几个属性或属性。对于操纵诸如id、rel和href之类的属性,jQuery 提供了.attr()和.removeAttr()方法。这些方法使更改属性变得简单。此外,jQuery 还允许我们一次修改多个属性,类似于我们使用.css()方法在第四章样式和动画中处理多个 CSS 属性的方式。
例如,我们可以轻松地一次设置链接的id、rel和title属性。让我们从一些示例 HTML 开始:
<h1 id="f-title">Flatland: A Romance of Many Dimensions</h1>
<div id="f-author">by Edwin A. Abbott</div>
<h2>Part 1, Section 3</h2>
<h3 id="f-subtitle">
Concerning the Inhabitants of Flatland
</h3>
<div id="excerpt">an excerpt</div>
<div class="chapter">
<p class="square">Our Professional Men and Gentlemen are
Squares (to which class I myself belong) and Five-Sided
Figures or <a
href="http://en.wikipedia.org/wiki/Pentagon">Pentagons
</a>.
</p>
<p class="nobility hexagon">Next above these come the
Nobility, of whom there are several degrees, beginning at
Six-Sided Figures, or <a
href="http://en.wikipedia.org/wiki/Hexagon">Hexagons</a>,
and from thence rising in the number of their sides till
they receive the honourable title of <a
href="http://en.wikipedia.org/wiki/Polygon">Polygonal</a>,
or many-Sided. Finally when the number of the sides
becomes so numerous, and the sides themselves so small,
that the figure cannot be distinguished from a <a
href="http://en.wikipedia.org/wiki/Circle">circle</a>, he
is included in the Circular or Priestly order; and this is
the highest class of all.
</p>
<p><span class="pull-quote">It is a <span class="drop">Law
of Nature</span> with us that a male child shall have
<strong>one more side</strong> than his father</span>, so
that each generation shall rise (as a rule) one step in
the scale of development and nobility. Thus the son of a
Square is a Pentagon; the son of a Pentagon, a Hexagon;
and so on.
</p>
<!-- . . . code continues . . . -->
</div>
获取示例代码
您可以从以下 GitHub 存储库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3。
现在,我们可以迭代<div class="chapter">内的每个链接,并逐个应用属性。如果我们需要为所有链接设置单个属性值,我们可以在我们的$(() => {})处理程序中用一行代码完成:
$(() => {
$('div.chapter a').attr({ rel: 'external' });
});
列表 5.1
就像.css()方法一样,.attr()也可以接受一对参数,第一个指定属性名,第二个是其新值。不过,更典型的是,我们提供一个键值对的对象,就像在 清单 5.1 中所做的那样。以下语法允许我们轻松地扩展我们的示例以一次修改多个属性:
$(() => {
$('div.chapter a')
.attr({
rel: 'external',
title: 'Learn more at Wikipedia'
});
});
清单 5.2
值回调
将一个简单对象传递给.attr()是一个直接的技巧,当我们希望每个匹配的元素具有相同的值时,它就足够了。然而,通常情况下,我们添加或更改的属性必须每次具有不同的值。一个常见的例子是,对于任何给定的文档,如果我们希望我们的 JavaScript 代码表现可预测,那么每个id值必须是唯一的。为每个链接设置唯一的id值,我们可以利用 jQuery 方法的另一个特性,如.css()和.each()--值回调。
值回调只是一个提供给参数的函数,而不是值。然后,对匹配集合中的每个元素调用此函数一次。从函数返回的任何数据都将用作属性的新值。例如,我们可以使用以下技术为每个元素生成不同的id值:
$(() => {
$('div.chapter a')
.attr({
rel: 'external',
title: 'Learn more at Wikipedia',
id: index => `wikilink-${index}`
});
});
清单 5.3
每次调用我们的值回调时,都会传递一个整数,指示迭代计数;在这里,我们正在使用它为第一个链接赋予一个id值wikilink-0,第二个wikilink-1,依此类推。
我们正在使用title属性邀请人们在维基百科了解更多有关链接术语的信息。到目前为止,我们使用的 HTML 标签中,所有链接都指向维基百科。但是,为了考虑到其他类型的链接,我们应该使选择器表达式更具体一些:
$(() => {
$('div.chapter a[href*="wikipedia"]')
.attr({
rel: 'external',
title: 'Learn more at Wikipedia',
id: index => `wikilink-${index}`
});
});
清单 5.4
要完成我们对.attr()方法的介绍,我们将增强这些链接的title属性,使其更具体地描述链接目标。再次,值回调是完成工作的正确工具:
$(() => {
$('div.chapter a[href*="wikipedia"]')
.attr({
rel: 'external',
title: function() {
return `Learn more about ${$(this).text()} at Wikipedia.`;
},
id: index => `wikilink-${index}`
});
});
清单 5.5
这次我们利用了值回调的上下文。就像事件处理程序一样,关键字this每次调用回调时都指向我们正在操作的 DOM 元素。在这里,我们将元素包装在一个 jQuery 对象中,以便我们可以使用.text()方法(在第四章中介绍的 Styling and Animating)来检索链接的文本内容。这使得每个链接标题与其他链接不同,如下面的屏幕截图所示:
数据属性
HTML5 数据属性允许我们将任意数据值附加到页面元素。然后,我们的 jQuery 代码可以使用这些值,以及修改它们。使用数据属性的原因是我们可以将控制它们的显示和行为的 DOM 属性与特定于我们的应用程序的数据分开。
使用data() jQuery 方法来读取数据值并更改数据值。让我们添加一些新功能,允许用户通过点击来标记段落为已读。我们还需要一个复选框,用于隐藏已标记为已读的段落。我们将使用数据属性来帮助我们记住哪些段落已标记为已读:
$(() => {
$('#hide-read')
.change((e) => {
if ($(e.target).is(':checked')) {
$('.chapter p')
.filter((i, p) => $(p).data('read'))
.hide();
} else {
$('.chapter p').show();
}
});
$('.chapter p')
.click((e) => {
const $elm = $(e.target);
$elm
.css(
'textDecoration',
$elm.data('read') ? 'none' : 'line-through'
)
.data('read', !$(e.target).data('read'));
});
});
列表 5.6
当您单击段落时,文本将被标记为已读:
正如您所看到的,点击事件处理程序在段落被点击时改变其视觉外观。但处理程序还做了其他事情--它切换了元素的read数据:data('read', !$(e.target).data('read'))。这让我们能够以一种不干扰我们可能设置的其他 HTML 属性的方式将应用程序特定的数据与元素绑定。
隐藏已读段落复选框的更改处理程序寻找具有此数据的段落。filter((i, p) => $(p).data('read'))调用只会返回具有值为true的read数据属性的段落。我们现在能够根据特定的应用程序数据来过滤元素。以下是隐藏已读段落后页面的外观:
我们将在本书的后期重新讨论一些使用 jQuery 处理数据的高级用法。
DOM 元素属性
正如前面提到的,HTML 属性 和 DOM 属性 之间存在微妙的区别。属性是页面 HTML 源代码中用引号括起来的值,而属性是 JavaScript 访问时的值。我们可以在 Chrome 等开发者工具中轻松观察属性和属性:
Chrome 开发者工具的元素检查器向我们展示了高亮显示的<p>元素具有名为class的属性,其值为square。在右侧面板中,我们可以看到该元素具有一个名为className的对应属性,其值为square。这说明了属性及其等效属性具有不同名称的情况之一。
在大多数情况下,属性和属性在功能上是可以互换的,并且 jQuery 会为我们处理命名不一致性。然而,有时我们确实需要注意两者之间的区别。一些 DOM 属性,如nodeName,nodeType,selectedIndex和childNodes,没有等效的属性,因此无法通过.attr()访问。此外,数据类型可能不同:例如,checked属性具有字符串值,而checked属性具有布尔值。对于这些布尔属性,最好测试和设置属性而不是属性,以确保跨浏览器行为的一致性。
我们可以使用.prop()方法从 jQuery 获取和设置属性:
// Get the current value of the "checked" property
const currentlyChecked = $('.my-checkbox').prop('checked');
// Set a new value for the "checked" property
$('.my-checkbox').prop('checked', false);
.prop()方法具有与.attr()相同的所有功能,例如接受一次设置多个值的对象和接受值回调函数。
表单控件的值
在尝试获取或设置表单控件的值时,属性和属性之间最麻烦的差异也许就是最为令人头疼的。对于文本输入,value属性等同于defaultValue属性,而不是value属性。对于select元素,通常通过元素的selectedIndex属性或其option元素的selected属性来获取值。
由于这些差异,我们应该避免使用.attr()——在select元素的情况下,甚至避免使用.prop()——来获取或设置表单元素的值。相反,我们可以使用 jQuery 为这些场合提供的.val()方法:
// Get the current value of a text input
const inputValue = $('#my-input').val();
// Get the current value of a select list
const selectValue = $('#my-select').val();
//Set the value of a single select list
$('#my-single-select').val('value3');
// Set the value of a multiple select list
$('#my-multi-select').val(['value1', 'value2']);
与.attr()和.prop()一样,.val()方法可以接受一个函数作为其设置器参数。借助其多功能的.val()方法,jQuery 再次让 Web 开发变得更加容易。
DOM 树操作
.attr()和.prop()方法是非常强大的工具,借助它们,我们可以对文档进行有针对性的更改。尽管如此,我们仍然没有看到如何更改文档的整体结构。要真正操作 DOM 树,你需要更多地了解位于jQuery库核心的函数。
$()函数再探讨
从本书的开头,我们一直在使用$()函数来访问文档中的元素。正如我们所见,这个函数充当了一个工厂的角色,产生了指向由 CSS 选择器描述的元素的新的 jQuery 对象。
$()函数的功能远不止于此。它还可以改变页面的内容。只需将一小段 HTML 代码传递给函数,我们就可以创建一个全新的 DOM 结构。
辅助功能提醒
我们应该再次牢记,将某些功能、视觉吸引力或文本信息仅提供给那些能够(并启用了)使用 JavaScript 的 Web 浏览器的人,存在固有的危险。重要信息应该对所有人可访问,而不仅仅是那些使用正确软件的人。
创建新元素
常见于 FAQ 页面的功能之一是在每个问题和答案对之后显示返回顶部链接。可以说这些链接没有任何语义作用,因此它们可以通过 JavaScript 合法地作为页面访问者子集的增强功能。在我们的示例中,我们将在每个段落后面添加一个返回顶部链接,以及返回顶部链接将指向的锚点。首先,我们简单地创建新元素:
$(() => {
$('<a href="#top">back to top</a>');
$('<a id="top"></a>');
});
列表 5.7
我们在第一行代码中创建了一个返回顶部链接,在第二行创建了链接的目标锚点。然而,页面上还没有出现返回顶部的链接。
虽然我们编写的两行代码确实创建了元素,但它们还没有将元素添加到页面上。我们需要告诉浏览器这些新元素应该放在哪里。为此,我们可以使用众多 jQuery 插入方法之一。
插入新元素
jQuery 库有许多可用于将元素插入文档的方法。每个方法都规定了新内容与现有内容的关系。例如,我们希望我们的返回顶部链接出现在每个段落后面,因此我们将使用适当命名的 .insertAfter() 方法来实现这一点:
$(() => {
$('<a href="#top">back to top</a>')
.insertAfter('div.chapter p');
$('<a id="top"></a>');
});
列表 5.8
因此,现在我们实际上已经将链接插入到页面中(并插入到 DOM 中),它们将出现在 <div class="chapter"> 中的每个段落之后:
请注意,新链接出现在自己的一行上,而不是在段落内部。这是因为 .insertAfter() 方法及其对应的 .insertBefore() 方法会在指定元素外部添加内容。
不幸的是,链接还不能使用。我们仍然需要插入带有 id="top" 的锚点。这一次,我们将使用一个在其他元素内部插入元素的方法:
$(() => {
$('<a href="#top">back to top</a>')
.insertAfter('div.chapter p');
$('<a id="top"></a>')
.prependTo('body');
});
列表 5.9
这段额外的代码将锚点插入在 <body> 标签的开头;换句话说,位于页面顶部。现在,使用链接的 .insertAfter() 方法和锚点的 .prependTo() 方法,我们有了一个完全功能的返回顶部链接集合。
一旦我们添加了相应的 .appendTo() 方法,我们现在就有了一个完整的选项集,用于在其他元素之前和之后插入新元素:
-
.insertBefore(): 在现有元素之外并且在其前面添加内容 -
.prependTo(): 在现有元素之内并且在其前面添加内容 -
.appendTo(): 在现有元素之内并且在其后面添加内容 -
.insertAfter(): 在现有元素之外并且在其后面添加内容
移动元素
在添加返回顶部链接时,我们创建了新的元素并将它们插入到页面中。还可以将页面上的元素从一个地方移动到另一个地方。这种插入的实际应用是动态放置和格式化脚注。一个脚注已经出现在我们用于此示例的原始 Flatland 文本中,但为了演示目的,我们还将指定文本的另外几部分作为脚注:
<p>How admirable is the Law of Compensation! <span
class="footnote">And how perfect a proof of the natural
fitness and, I may almost say, the divine origin of the
aristocratic constitution of the States of Flatland!</span>
By a judicious use of this Law of Nature, the Polygons and
Circles are almost always able to stifle sedition in its
very cradle, taking advantage of the irrepressible and
boundless hopefulness of the human mind.…
</p>
我们的 HTML 文档包含三个脚注;上一个段落包含一个示例。脚注文本位于段落文本内部,使用 <span class="footnote"></span> 进行分隔。通过以这种方式标记 HTML 文档,我们可以保留脚注的上下文。样式表中应用的 CSS 规则使脚注变为斜体,因此受影响的段落最初看起来像下面这样:
现在,我们需要抓取脚注并将它们移动到文档底部。具体来说,我们将它们插入在<div class="chapter">和<div id="footer">之间。
请记住,即使在隐式迭代的情况下,处理元素的顺序也是精确定义的,从 DOM 树的顶部开始并向下工作。由于在页面上保持脚注的正确顺序很重要,我们应该使用.insertBefore('#footer')。这将使每个脚注直接放在<div id="footer">元素之前,以便第一个脚注放在<div class="chapter">和<div id="footer">之间,第二个脚注放在第一个脚注和<div id="footer">之间,依此类推。另一方面,使用.insertAfter('div.chapter')会导致脚注以相反的顺序出现。
到目前为止,我们的代码看起来像下面这样:
$(() => {
$('span.footnote').insertBefore('#footer');
});
图 5.10
脚注位于<span>标签中,默认情况下显示为内联,一个紧挨着另一个,没有分隔。但是,我们在 CSS 中已经预料到了这一点,在span.footnote元素处于<div class="chapter">之外时,给予了display值为block。因此,脚注现在开始成形:
现在,脚注已经位于正确的位置,但是仍然有很多工作可以做。一个更健壮的脚注解决方案应该执行以下操作:
-
对每个脚注编号。
-
使用脚注的编号标记从文本中提取每个脚注的位置。
-
从文本位置创建到其匹配脚注的链接,并从脚注返回到从文本中提取每个脚注的位置,使用脚注的编号。
包装元素
为了给脚注编号,我们可以在标记中显式添加数字,但是在这里我们可以利用标准的有序列表元素,它会为我们自动编号。为此,我们需要创建一个包含所有脚注的<ol>元素和一个单独包含每个脚注的<li>元素。为了实现这一点,我们将使用包装方法。
在将元素包装在另一个元素中时,我们需要明确我们是想让每个元素都包装在自己的容器中,还是所有元素都包装在一个单一的容器中。对于我们的脚注编号,我们需要两种类型的包装器:
$(() => {
$('span.footnote')
.insertBefore('#footer')
.wrapAll('<ol id="notes"></ol>')
.wrap('<li></li>');
});
图 5.11
一旦我们在页脚之前插入了脚注,我们就使用.wrapAll()将整个集合包装在一个单独的<ol>元素内。然后,我们继续使用.wrap()将每个单独的脚注包装在其自己的<li>元素内。我们可以看到这样创建了正确编号的脚注:
现在,我们已经准备好标记并编号我们提取脚注的位置。为了以简单直接的方式做到这一点,我们需要重写我们现有的代码,使其不依赖于隐式迭代。
显式迭代
.each()方法充当显式迭代器,与最近添加到 JavaScript 语言中的forEach数组迭代器非常相似。当我们想要对匹配的每个元素使用的代码过于复杂时,可以使用.each()方法。它接受一个回调函数,该函数将对匹配集合中的每个元素调用一次。
$(() => {
const $notes = $('<ol id="notes"></ol>')
.insertBefore('#footer');
$('span.footnote')
.each((i, span) => {
$(span)
.appendTo($notes)
.wrap('<li></li>');
});
});
清单 5.12
我们这里的更改动机很快就会变得清晰。首先,我们需要了解传递给我们的.each()回调的信息。
在清单 5.12中,我们使用span参数创建一个指向单个脚注<span>的 jQuery 对象,然后将该元素追加到脚注 <ol> 中,最后将脚注包装在一个 <li> 元素中。
为了标记从中提取脚注的文本位置,我们可以利用.each()回调的参数。该参数提供了迭代计数,从0开始,并在每次调用回调时递增。因此,该计数器始终比脚注的数量少 1。在生成文本中的适当标签时,我们将考虑到这一事实:
$(() => {
const $notes = $('<ol id="notes"></ol>')
.insertBefore('#footer');
$('span.footnote')
.each((i, span) => {
$(`<sup>${i + 1}</sup>`)
.insertBefore(span);
$(span)
.appendTo($notes)
.wrap('<li></li>');
});
});
清单 5.13
现在,在每个脚注被从文本中取出并放置在页面底部之前,我们创建一个包含脚注编号的新 <sup> 元素,并将其插入到文本中。这里的操作顺序很重要;我们需要确保标记被插入到移动脚注之前,否则我们将丢失其初始位置的追踪。
再次查看我们的页面,现在我们可以看到脚注标记出现在原来的内联脚注位置上:
使用反向插入方法
在清单 5.13中,我们在一个元素之前插入内容,然后将该元素追加到文档的另一个位置。通常,在 jQuery 中处理元素时,我们可以使用链式操作来简洁高效地执行多个操作。但是在这里,我们无法做到这一点,因为this是.insertBefore()的目标,同时也是.appendTo()的主语。反向插入方法将帮助我们克服这个限制。
每个插入方法,如.insertBefore()或.appendTo(),都有一个对应的反向方法。反向方法执行的任务与标准方法完全相同,但主语和目标被颠倒了。例如:
$('<p>Hello</p>').appendTo('#container');
与下面相同:
$('#container').append('<p>Hello</p>');
使用.before(),即.insertBefore()的反向形式,现在我们可以重构我们的代码以利用链式操作:
$(() => {
const $notes = $('<ol id="notes"></ol>')
.insertBefore('#footer');
$('span.footnote')
.each((i, span) => {
$(span)
.before(`<sup>${i + 1}</sup>`)
.appendTo($notes)
.wrap('<li></li>');
});
});
清单 5.14
插入方法回调
反向插入方法可以接受一个函数作为参数,就像.attr()和.css()一样。这个函数会针对每个目标元素调用一次,并且应返回要插入的 HTML 字符串。我们可以在这里使用这种技术,但由于我们将遇到每个脚注的几种这样的情况,因此单个的.each()调用最终将成为更清晰的解决方案。
现在我们准备处理我们清单中的最后一步:为文本位置创建到相应脚注的链接,以及从脚注返回到文本位置。为了实现这一点,我们需要每个脚注四个标记:在文本中和脚注之后各一个链接,以及在相同位置的两个id属性。因为.before()方法的参数即将变得复杂,这是一个引入新的字符串创建的好时机。
在清单 5.14 中,我们使用模板字符串准备了我们的脚注标记。这是一种非常有用的技术,但是当连接大量字符串时,它可能开始显得混乱。相反,我们可以使用数组方法.join()来构建更大的字符串。以下语句具有相同的效果:
var str = 'a' + 'b' + 'c';
var str = `${'a'}${'b'}${'c'}`;
var str = ['a', 'b', 'c'].join('');
尽管在这个例子中需要输入更多的字符,但.join()方法可以在原本难以阅读的字符串连接或字符串模板时提供清晰度。让我们再次看一下我们的代码,这次使用.join()来创建字符串:
$(() => {
const $notes = $('<ol id="notes"></ol>')
.insertBefore('#footer');
$('span.footnote')
.each((i, span) => {
$(span)
.before([
'<sup>',
i + 1,
'</sup>'
].join(''))
.appendTo($notes)
.wrap('<li></li>');
});
});
项目清单 5.15
使用这种技术,我们可以为脚注标记增加一个到页面底部的链接,以及一个唯一的id值。一边做这些,我们还将为<li>元素添加一个id,这样链接就有了一个目标,如下面的代码片段所示:
$(() => {
const $notes = $('<ol id="notes"></ol>')
.insertBefore('#footer');
$('span.footnote')
.each((i, span) => {
$(span)
.before([
'<a href="#footnote-',
i + 1,
'" id="context-',
i + 1,
'" class="context">',
'<sup>',
i + 1,
'</sup></a>'
].join(''))
.appendTo($notes)
.wrap('<li></li>');
});
});
项目清单 5.16
在额外的标记放置后,每个脚注标记现在都链接到文档底部的对应脚注。 现在唯一剩下的就是创建一个从脚注返回到其上下文的链接。为此,我们可以使用.appendTo()方法的反向,即.append():
$(() => {
const $notes = $('<ol id="notes"></ol>')
.insertBefore('#footer');
$('span.footnote')
.each((i, span) => {
$(span)
.before([
'<a href="#footnote-',
i + 1,
'" id="context-',
i + 1,
'" class="context">',
'<sup>',
i + 1,
'</sup></a>'
].join(''))
.appendTo($notes)
.append([
' (<a href="#context-',
i + 1,
'">context</a>)'
].join(''))
.wrap('<li></li>');
});
});
项目清单 5.17
请注意,href标签指向了对应标记的id值。在下面的屏幕截图中,您可以再次看到脚注,不同的是这次每个脚注后都附加了新链接:
复制元素
到目前为止,在本章中,我们已经插入了新创建的元素,将元素从文档中的一个位置移动到另一个位置,并将新元素包裹在现有元素周围。但是,有时,我们可能想要复制元素。例如,出现在页面页眉中的导航菜单也可以复制并放置在页脚中。每当元素可以被复制以增强页面的视觉效果时,我们可以让 jQuery 承担繁重的工作。
对于复制元素,jQuery 的.clone()方法正是我们需要的;它接受任何匹配元素集并为以后使用创建它们的副本。就像我们前面在本章中探讨过的$()函数的元素创建过程一样,复制的元素在应用插入方法之前不会出现在文档中。
例如,下面的行创建了<div class="chapter">中第一个段落的副本:
$('div.chapter p:eq(0)').clone();
光靠这些还不足以改变页面的内容。我们可以使克隆的段落出现在<div class="chapter">之前用插入方法:
$('div.chapter p:eq(0)')
.clone()
.insertBefore('div.chapter');
这将导致第一个段落出现两次。因此,使用一个熟悉的类比,.clone() 与插入方法的关系就像 复制 与 粘贴 一样。
带事件的克隆
默认情况下,.clone() 方法不会复制绑定到匹配元素或其任何后代的任何事件。然而,它可以接受一个布尔参数(当设置为 true(.clone(true))时),也会克隆事件。这种方便的事件克隆使我们避免了手动重新绑定事件,正如 第三章 中讨论的那样,处理事件。
用于引文的克隆
许多网站,就像它们的印刷对应物一样,使用 引文 来强调文本的小部分并吸引读者的注意。引文简单地是主文档的摘录,它以特殊的图形处理与文本一起呈现。我们可以通过 .clone() 方法轻松实现这种修饰。首先,让我们再次看一下示例文本的第三段:
<p>
<span class="pull-quote">It is a Law of Nature
<span class="drop">with us</span> that a male child shall
have <strong>one more side</strong> than his father</span>,
so that each generation shall rise (as a rule) one step in
the scale of development and nobility. Thus the son of a
Square is a Pentagon; the son of a Pentagon, a Hexagon; and
so on.
</p>
注意段落以 <span class="pull-quote"> 开始。这是我们将要复制的类。一旦在另一个位置粘贴了该 <span> 标签中的复制文本,我们就需要修改其样式属性以使其与其余文本区分开。
为了实现这种类型的样式,我们将在复制的 <span> 中添加一个 pulled 类。在我们的样式表中,该类接收以下样式规则:
.pulled {
position: absolute;
width: 120px;
top: -20px;
right: -180px;
padding: 20px;
font: italic 1.2em "Times New Roman", Times, serif;
background: #e5e5e5;
border: 1px solid #999;
border-radius: 8px;
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.6);
}
具有此类的元素通过应用背景、边框、字体等样式规则在视觉上与主内容区分开来。最重要的是,它是绝对定位的,距离 DOM 中最近的(absolute 或 relative)定位的祖先元素的顶部 20 像素,并且向右偏移 20 像素。如果没有祖先元素应用了定位(除了 static 之外),引用的引用将相对于文档 <body> 定位。因此,在 jQuery 代码中,我们需要确保克隆的引文的父元素设置了 position:relative。
CSS 定位计算
尽管顶部定位相当直观,但可能一开始不清楚引文框将如何定位到其定位父级的右侧 20 像素。我们首先从引文框的总宽度推导数字,这是 width 属性的值加上左右填充的值,或 145px + 5px + 10px = 160px。然后,我们设置引文的 right 属性。一个值为 0 将使引文的右侧与其父元素的右侧对齐。因此,为了将其左侧定位到父元素的右侧 20 像素处,我们需要将其向负方向移动超过其总宽度的 20 像素,即 -180px。
现在,我们可以考虑应用此样式所需的 jQuery 代码。我们将从选择器表达式开始,找到所有 <span class="pull-quote"> 元素,并像我们刚讨论的那样为每个父元素应用 position: relative 样式:
$(() => {
$('span.pull-quote')
.each((i, span) => {
$(span)
.parent()
.css('position', 'relative');
});
});
列表 5.18
接下来,我们需要创建引用本身,利用我们准备好的 CSS。我们需要克隆每个 <span> 标签,将 pulled 类添加到副本,并将其插入到其父段落的开头:
$(() => {
$('span.pull-quote')
.each((i, span) => {
$(span)
.clone()
.addClass('pulled')
.prependTo(
$(span)
.parent()
.css('position', 'relative')
);
});
});
列表 5.19
因为我们在引用处使用了绝对定位,所以它在段落中的位置是无关紧要的。只要它保持在段落内部,根据我们的 CSS 规则,它将相对于段落的顶部和右侧定位。
引用现在出现在其原始段落旁边,正如预期的那样:
这是一个不错的开始。对于我们的下一个改进,我们将稍微清理引用内容。
内容获取器和设置器方法
修改引用并使用省略号来保持内容简洁将是很好的。为了演示这一点,我们在示例文本中的几个单词中包裹了一个 <span class="drop"> 标签。
完成此替换的最简单方法是直接指定要替换旧实体的新 HTML 实体。.html() 方法非常适合这个目的:
$(() => {
$('span.pull-quote')
.each((i, span) => {
$(span)
.clone()
.addClass('pulled')
.find('span.drop')
.html('…')
.end()
.prependTo(
$(span)
.parent()
.css('position', 'relative')
);
});
});
列表 5.20
列表 5.20 中的新行依赖于我们在第二章中学到的 DOM 遍历技巧,选择元素。我们使用 .find() 在引用中搜索任何 <span class="drop"> 元素,对它们进行操作,然后通过调用 .end() 返回到引用本身。在这些方法之间,我们调用 .html() 将内容更改为省略号(使用适当的 HTML 实体)。
在没有参数的情况下调用 .html() 会返回匹配元素内的 HTML 实体的字符串表示。有了参数,元素的内容将被提供的 HTML 实体替换。当使用此技术时,我们必须小心只指定一个有效的 HTML 实体,并正确地转义特殊字符。
指定的单词现已被省略号替换:
引用通常不保留其原始字体格式,比如这个示例中的粗体文本。我们真正想显示的是 <span class="pull-quote"> 的文本,不包含任何 <strong>、<em>、<a href> 或其他内联标签。为了将所有引用的 HTML 实体替换为剥离后的仅文本版本,我们可以使用 .html() 方法的伴随方法 .text()。
像 .html() 一样,.text() 方法可以检索匹配元素的内容或用新字符串替换其内容。但与 .html() 不同的是,.text() 总是获取或设置纯文本字符串。当 .text() 检索内容时,所有包含的标签都将被忽略,HTML 实体将被转换为普通字符。当它设置内容时,特殊字符如 < 将被转换为它们的 HTML 实体等价物:
$(() => {
$('span.pull-quote')
.each((i, span) => {
$(span)
.clone()
.addClass('pulled')
.find('span.drop')
.html('…')
.end()
.text((i, text) => text)
.prependTo(
$(span)
.parent()
.css('position', 'relative')
);
});
});
列表 5.21
使用text()检索值时,会去除标记。这正是我们尝试实现的内容。与你目前学习的其他一些 jQuery 函数一样,text()接受一个函数。返回值用于设置元素的文本,而当前文本则作为第二个参数传入。因此,要从元素文本中删除标记,只需调用text((i, text) => text)。太棒了!
以下是这种方法的结果:
DOM 操作方法简介
jQuery 提供的大量 DOM 操作方法根据任务和目标位置而异。我们在这里没有涵盖所有内容,但大多数都类似于我们已经见过的方法,更多内容将在第十二章,高级 DOM 操作中讨论。以下概要可作为我们可以使用哪种方法来完成哪种任务的提醒:
-
若要从 HTML 中创建新元素,请使用
$()函数 -
若要在每个匹配元素内部插入新元素,请使用以下函数:
-
.append() -
.appendTo() -
.prepend() -
.prependTo()
-
-
若要在每个匹配元素旁边插入新元素,请使用以下函数:
-
.after() -
.insertAfter() -
.before() -
.insertBefore()
-
-
若要在每个匹配元素周围插入新元素,请使用以下函数:
-
.wrap() -
.wrapAll() -
.wrapInner()
-
-
若要用新元素或文本替换每个匹配元素,请使用以下函数:
-
.html() -
.text() -
.replaceAll() -
.replaceWith()
-
-
若要在每个匹配元素内部删除元素,请使用以下函数:
.empty()
-
若要删除文档中每个匹配元素及其后代,而实际上不删除它们,请使用以下函数:
-
.remove() -
.detach()
-
摘要
在本章中,我们使用 jQuery 的 DOM 修改方法创建、复制、重新组装和美化内容。我们将这些方法应用于单个网页,将一些通用段落转换为带有脚注、拉引用、链接和样式化的文学摘录。这一章向我们展示了使用 jQuery 添加、删除和重新排列页面内容是多么容易。此外,你已经学会了如何对页面元素的 CSS 和 DOM 属性进行任何想要的更改。
接下来,我们将通过 jQuery 的 Ajax 方法进行一次往返旅程到服务器。
进一步阅读
DOM 操作的主题将在第十二章,高级 DOM 操作中进行更详细的探讨。DOM 操作方法的完整列表可在本书的附录 B*,快速参考*,或在官方 jQuery 文档api.jquery.com/中找到。
练习
挑战练习可能需要使用官方 jQuery 文档http://api.jquery.com/。
-
改变引入回到顶部链接的代码,使得链接只在第四段后出现。
-
当点击回到顶部链接时,在链接后添加一个新段落,其中包含消息“你已经在这里了”。确保链接仍然可用。
-
当点击作者的名字时,将其加粗(通过添加元素,而不是操作类或 CSS 属性)。
-
挑战:在对加粗的作者名字进行后续点击时,移除已添加的
<b>元素(从而在加粗和正常文本之间切换)。 -
挑战:对每个章节段落添加一个
inhabitants类,而不调用.addClass()。确保保留任何现有的类。
第六章:使用 Ajax 发送数据
术语 Asynchronous JavaScript and XML(Ajax)是由 Jesse James Garrett 在 2005 年创造的。此后,它已经代表了许多不同的事物,因为该术语包含了一组相关的能力和技术。在其最基本的层次上,Ajax 解决方案包括以下技术:
-
JavaScript:用于捕获与用户或其他与浏览器相关的事件的交互,并解释来自服务器的数据并在页面上呈现它
-
XMLHttpRequest:这允许在不中断其他浏览器任务的情况下向服务器发出请求
-
文本数据: 服务器提供的数据格式可以是 XML、HTML 或 JSON 等。
Ajax 将静态网页转变为交互式网络应用程序。毫不奇怪,浏览器在实现XMLHttpRequest对象时并不完全一致,但 jQuery 会帮助我们。
在本章中,我们将涵盖:
-
在不刷新页面的情况下从服务器加载数据
-
从浏览器中的 JavaScript 发送数据回服务器
-
解释各种格式的数据,包括 HTML、XML 和 JSON
-
向用户提供有关 Ajax 请求状态的反馈
按需加载数据
Ajax 只是一种从服务器加载数据到网络浏览器中而无需刷新页面的方法。这些数据可以采用许多形式,而当数据到达时,我们有许多选项可以处理它。我们将通过使用不同的方法执行相同的基本任务来看到这一点。
我们将构建一个页面,显示按字典条目起始字母分组的条目。定义页面内容区域的 HTML 将如下所示:
<div id="dictionary">
</div>
我们的页面一开始没有内容。我们将使用 jQuery 的各种 Ajax 方法来填充这个 <div> 标记,以显示字典条目。
获取示例代码
您可以从以下 GitHub 仓库访问示例代码:github.com/PacktPublishing/Learning-jQuery-3。
我们需要一种触发加载过程的方法,所以我们将添加一些链接供我们的事件处理程序依附:
<div class="letters">
<div class="letter" id="letter-a">
<h3><a href="entries-a.html">A</a></h3>
</div>
<div class="letter" id="letter-b">
<h3><a href="entries-a.html">B</a></h3>
</div>
<div class="letter" id="letter-c">
<h3><a href="entries-a.html">C</a></h3>
</div>
<div class="letter" id="letter-d">
<h3><a href="entries-a.html">D</a></h3>
</div>
<!-- and so on -->
</div>
这些简单的链接将带领我们到列出该字母字典条目的页面。我们将采用渐进式增强的方法,允许这些链接在不加载完整页面的情况下操作页面。应用基本样式后,这个 HTML 将产生如下页面:
现在,我们可以专注于将内容放到页面上。
追加 HTML
Ajax 应用程序通常不过是对一块 HTML 的请求。这种技术有时被称为 Asynchronous HTTP and HTML(AHAH),在 jQuery 中几乎很容易实现。首先,我们需要一些要插入的 HTML,我们将其放置在一个名为 a.html 的文件中,与我们的主文档一起。这个辅助 HTML 文件的开头如下:
<div class="entry">
<h3 class="term">ABDICATION</h3>
<div class="part">n.</div>
<div class="definition">
An act whereby a sovereign attests his sense of the high
temperature of the throne.
<div class="quote">
<div class="quote-line">Poor Isabella's Dead, whose
abdication</div>
<div class="quote-line">Set all tongues wagging in the
Spanish nation.</div>
<div class="quote-line">For that performance 'twere
unfair to scold her:</div>
<div class="quote-line">She wisely left a throne too
hot to hold her.</div>
<div class="quote-line">To History she'll be no royal
riddle —</div>
<div class="quote-line">Merely a plain parched pea that
jumped the griddle.</div>
<div class="quote-author">G.J.</div>
</div>
</div>
</div>
<div class="entry">
<h3 class="term">ABSOLUTE</h3>
<div class="part">adj.</div>
<div class="definition">
Independent, irresponsible. An absolute monarchy is one
in which the sovereign does as he pleases so long as he
pleases the assassins. Not many absolute monarchies are
left, most of them having been replaced by limited
monarchies, where the sovereign's power for evil (and for
good) is greatly curtailed, and by republics, which are
governed by chance.
</div>
</div>
页面继续以这种 HTML 结构的更多条目。单独渲染的话,a.html 看起来相当简单:
请注意,a.html 不是一个真正的 HTML 文档;它不包含 <html>、<head> 或 <body>,这些通常是必需的。我们通常将这样的文件称为部分或片段;它的唯一目的是被插入到另一个 HTML 文档中,我们现在将这样做:
$(() => {
$('#letter-a a')
.click((e) => {
e.preventDefault()
$('#dictionary').load('a.html');
});
});
第 6.1 节
.load() 方法为我们做了所有繁重的工作。我们使用普通的 jQuery 选择器指定 HTML 片段的目标位置,然后将要加载的文件的 URL 作为参数传递。现在,当单击第一个链接时,文件将被加载并放置在 <div id="dictionary"> 内。一旦插入新的 HTML,浏览器就会渲染它:
注意 HTML 现在已经有样式了,而之前是原样呈现。这是由于主文档中的 CSS 规则;一旦插入新的 HTML 片段,规则也会应用于其元素。
在测试这个示例时,当单击按钮时,字典定义可能会立即出现。这是在本地工作应用程序时的一个危险;很难预测跨网络传输文档时的延迟或中断。假设我们添加一个警报框,在加载定义后显示:
$(() => {
$('#letter-a a')
.click((e) => {
e.preventDefault()
$('#dictionary').load('a.html');
alert('Loaded!');
});
});
第 6.2 节
我们可能会从这段代码的结构中假设警报只能在执行加载后显示。JavaScript 的执行是同步的,严格按顺序一个任务接一个任务执行。
然而,当这段特定的代码在生产 Web 服务器上测试时,由于网络延迟,警报将在加载完成之前出现并消失。这是因为所有 Ajax 调用默认是异步的。异步加载意味着一旦发出检索 HTML 片段的 HTTP 请求,脚本执行立即恢复而不等待。稍后,浏览器收到来自服务器的响应并处理它。这是期望的行为;锁定整个 Web 浏览器等待数据检索是不友好的。
如果必须延迟动作直到加载完成,jQuery 为此提供了一个回调函数。我们已经在第四章中看到了回调,在样式和动画中使用它们在效果完成后执行操作。Ajax 回调执行类似的功能,在从服务器接收数据后执行。我们将在下一个示例中使用此功能,学习如何从服务器读取 JSON 数据。
处理 JavaScript 对象
根据需要按需获取完整形式的 HTML 非常方便,但这意味着必须传输有关 HTML 结构的大量信息以及实际内容。有时我们希望尽可能少地传输数据,并在数据到达后进行处理。在这种情况下,我们需要以 JavaScript 可以遍历的结构检索数据。
借助 jQuery 的选择器,我们可以遍历获取的 HTML 并对其进行操作,但原生 JavaScript 数据格式涉及的数据量较少,处理起来的代码也较少。
检索 JSON
正如我们经常看到的那样,JavaScript 对象只是一组键值对,并且可以用花括号({})简洁地定义。另一方面,JavaScript 数组是用方括号([])即时定义的,并且具有隐式键,即递增整数。结合这两个概念,我们可以轻松表达一些非常复杂和丰富的数据结构。
术语JavaScript 对象表示法(JSON)是由 Douglas Crockford 创造的,以利用这种简单的语法。这种表示法可以提供简洁的替代方法来替代臃肿的 XML 格式:
{
"key": "value",
"key 2": [
"array",
"of",
"items"
]
}
尽管基于 JavaScript 对象字面量和数组字面量,但 JSON 对其语法要求更具规范性,对其允许的值更具限制性。例如,JSON 指定所有对象键以及所有字符串值必须用双引号括起来。此外,函数不是有效的 JSON 值。由于其严格性,开发人员应避免手动编辑 JSON,而应依赖于诸如服务器端脚本之类的软件来正确格式化它。
有关 JSON 的语法要求、一些潜在优势以及它在许多编程语言中的实现的信息,请访问json.org/。
我们可以以许多方式使用此格式对数据进行编码。为了说明一种方法,我们将一些字典条目放入一个名为 b.json 的 JSON 文件中:
[
{
"term": "BACCHUS",
"part": "n.",
"definition": "A convenient deity invented by the...",
"quote": [
"Is public worship, then, a sin,",
"That for devotions paid to Bacchus",
"The lictors dare to run us in,",
"And resolutely thump and whack us?"
],
"author": "Jorace"
},
{
"term": "BACKBITE",
"part": "v.t.",
"definition": "To speak of a man as you find him when..."
},
{
"term": "BEARD",
"part": "n.",
"definition": "The hair that is commonly cut off by..."
},
... file continues ...
要检索此数据,我们将使用 $.getJSON() 方法,该方法获取文件并对其进行处理。当数据从服务器到达时,它只是一个 JSON 格式的文本字符串。$.getJSON() 方法解析此字符串并向调用代码提供生成的 JavaScript 对象。
使用全局 jQuery 函数
到目前为止,我们使用的所有 jQuery 方法都附加在我们用 $() 函数构建的 jQuery 对象上。选择器允许我们指定一组要处理的 DOM 节点,并且这些方法以某种方式对其进行操作。然而,$.getJSON() 函数是不同的。它没有逻辑 DOM 元素可以应用;结果对象必须提供给脚本,而不是注入到页面中。因此,getJSON() 被定义为全局 jQuery 对象的方法(由 jQuery 库一次定义的单个对象,称为 jQuery 或 $),而不是单个 jQuery 对象实例的方法(由 $() 函数返回的对象)。
如果 $ 是一个类 $.getJSON() 将是一个类方法。对于我们的目的,我们将把这种类型的方法称为全局函数;实际上,它们是使用 jQuery 命名空间的函数,以避免与其他函数名称冲突。
要使用此函数,我们像以前一样将文件名传递给它:
$(() => {
$('#letter-b a')
.click((e) => {
e.preventDefault();
$.getJSON('b.json');
});
});
列表 6.3
当我们单击链接时,此代码似乎没有任何效果。函数调用加载文件,但我们还没有告诉 JavaScript 如何处理生成的数据。为此,我们需要使用回调函数。
$.getJSON() 函数接受第二个参数,这是在加载完成时调用的函数。如前所述,Ajax 调用是异步的,回调提供了一种等待数据传输完成而不是立即执行代码的方法。回调函数还接受一个参数,其中填充了生成的数据。所以,我们可以写:
$(() => {
$('#letter-b a')
.click((e) => {
e.preventDefault();
$.getJSON('b.json', (data) => {});
});
});
列表 6.4
在这个函数内部,我们可以使用 data 参数根据需要遍历 JSON 结构。我们需要迭代顶级数组,为每个项目构建 HTML。我们将使用数据数组的 reduce() 方法将其转换为 HTML 字符串,然后将其插入文档中。reduce() 方法接受一个函数作为参数,并为数组的每个项返回结果的一部分:
$(() => {
$('#letter-b a')
.click((e) => {
e.preventDefault();
$.getJSON('b.json', (data) => {
const html = data.reduce((result, entry) => `
${result}
<div class="entry">
<h3 class="term">${entry.term}</h3>
<div class="part">${entry.part}</div>
<div class="definition">
${entry.definition}
</div>
</div>
`, '');
$('#dictionary')
.html(html);
});
});
});
列表 6.5
我们使用模板字符串来构建每个数组项的 HTML 内容。result 参数是上一个数组项的值。使用这种方法,通过字符串拼接,可以更容易地看到 HTML 结构。一旦为每个条目构建了所有的 HTML,我们就用 .html() 将其插入到 <div id="dictionary"> 中,替换可能已经存在的任何内容。
安全的 HTML
这种方法假定数据对 HTML 消费是安全的;例如,它不应该包含任何杂乱的 < 字符。
唯一剩下的就是处理带引号的条目,我们可以通过实现一对使用 reduce() 技术构建字符串的辅助函数来完成:
$(() => {
const formatAuthor = entry =>
entry.author ?
`<div class="quote-author">${entry.author}</div>` :
'';
const formatQuote = entry =>
entry.quote ?
`
<div class="quote">
${entry.quote.reduce((result, q) => `
${result}
<div class="quote-line">${q}</div>
`, '')}
${formatAuthor(entry)}
</div>
` : '';
$('#letter-b a')
.click((e) => {
e.preventDefault();
$.getJSON('b.json', (data) => {
const html = data.reduce((result, entry) => `
${result}
<div class="entry">
<h3 class="term">${entry.term}</h3>
<div class="part">${entry.part}</div>
<div class="definition">
${entry.definition}
${formatQuote(entry)}
</div>
</div>
`, '');
$('#dictionary')
.html(html);
});
});
});
列表 6.6
有了这段代码,我们可以单击 B 链接并确认我们的结果。词典条目如预期的那样显示在页面的右侧:
JSON 格式简洁,但并不宽容。每个括号、大括号、引号和逗号必须存在且被计算在内,否则文件将无法加载。在某些情况下,我们甚至不会收到错误消息;脚本会悄无声息地失败。
执行脚本
有时,我们不希望在页面首次加载时检索到所有将需要的 JavaScript。在某些用户交互发生之前,我们可能不知道需要哪些脚本。我们可以在需要时动态引入 <script> 标签,但更加优雅的注入附加代码的方法是让 jQuery 直接加载 .js 文件。
拉取脚本与加载 HTML 片段一样简单。在这种情况下,我们使用 $.getScript() 函数,它——与其兄弟们一样——接受指向脚本文件的 URL:
$(() => {
$('#letter-c a')
.click((e) => {
e.preventDefault();
$.getScript('c.js');
});
});
列表 6.7
在我们的最后一个示例中,我们需要处理结果数据,以便我们可以对加载的文件执行一些有用的操作。不过,对于脚本文件,处理是自动的;脚本只是简单地运行。
以这种方式获取的脚本在当前页面的全局上下文中运行。这意味着它们可以访问所有全局定义的函数和变量,特别是包括 jQuery 本身。因此,我们可以仿照 JSON 示例,在脚本执行时准备和插入 HTML 到页面上,并将此代码放在c.js中:
const entries = [
{
"term": "CALAMITY",
"part": "n.",
"definition": "A more than commonly plain and..."
},
{
"term": "CANNIBAL",
"part": "n.",
"definition": "A gastronome of the old school who..."
},
{
"term": "CHILDHOOD",
"part": "n.",
"definition": "The period of human life intermediate..."
}
// and so on
];
const html = entries.reduce((result, entry) => `
${result}
<div class="entry">
<h3 class="term">${entry.term}</h3>
<div class="part">${entry.part}</div>
<div class="definition">
${entry.definition}
</div>
</div>
`, '');
$('#dictionary')
.html(html);
现在,点击 C 链接会得到预期的结果,显示相应的字典条目。
加载 XML 文档
XML 是 Ajax 首字母缩写的一部分,但我们实际上还没有加载任何 XML。这样做很简单,而且与 JSON 技术非常相似。首先,我们需要一个 XML 文件,d.xml,其中包含我们希望显示的一些数据:
<?xml version="1.0" encoding="UTF-8"?>
<entries>
<entry term="DEFAME" part="v.t.">
<definition>
To lie about another. To tell the truth about another.
</definition>
</entry>
<entry term="DEFENCELESS" part="adj.">
<definition>
Unable to attack.
</definition>
</entry>
<entry term="DELUSION" part="n.">
<definition>
The father of a most respectable family, comprising
Enthusiasm, Affection, Self-denial, Faith, Hope,
Charity and many other goodly sons and daughters.
</definition>
<quote author="Mumfrey Mappel">
<line>All hail, Delusion! Were it not for thee</line>
<line>The world turned topsy-turvy we should see;
</line>
<line>For Vice, respectable with cleanly fancies,
</line>
<line>Would fly abandoned Virtue's gross advances.
</line>
</quote>
</entry>
</entries>
当然,这些数据可以用许多方式表达,有些方式更接近我们早期用于 HTML 或 JSON 的结构。然而,在这里,我们正在说明 XML 的一些特性,以使其对人类更加可读,例如使用term和part属性而不是标签。
我们将以熟悉的方式开始我们的函数:
$(() => {
$('#letter-d a')
.click((e) => {
e.preventDefault();
$.get('d.xml', (data) => {
});
});
});
列表 6.8
这次,是$.get()函数完成了我们的工作。通常,此函数只是获取所提供 URL 的文件,并将纯文本提供给回调函数。但是,如果由于其服务器提供的 MIME 类型而已知响应为 XML,则回调函数将交给 XML DOM 树。
幸运的是,正如我们已经看到的,jQuery 具有实质性的 DOM 遍历功能。我们可以像在 HTML 上一样在 XML 文档上使用正常的.find()、.filter()和其他遍历方法:
$(() => {
$('#letter-d a')
.click((e) => {
const formatAuthor = entry =>
$(entry).attr('author') ?
`
<div class="quote-author">
${$(entry).attr('author')}
</div>
` : '';
const formatQuote = entry =>
$(entry).find('quote').length ?
`
<div class="quote">
${$(entry)
.find('quote')
.get()
.reduce((result, q) => `
${result}
<div class="quote-line">
${$(q).text()}
</div>
`, '')}
${formatAuthor(entry)}
</div>
` : '';
e.preventDefault();
$.get('d.xml', (data) => {
const html = $(data)
.find('entry')
.get()
.reduce((result, entry) => `
${result}
<div class="entry">
<h3 class="term">${$(entry).attr('term')}</h3>
<div class="part">${$(entry).attr('part')}</div>
<div class="definition">
${$(entry).find('definition').text()}
${formatQuote(entry)}
</div>
</div>
`, '');
$('#dictionary')
.html(html);
});
});
});
列表 6.9
当点击 D 链接时,这将产生预期的效果:
这是我们已经了解的 DOM 遍历方法的一种新用法,揭示了 jQuery 的 CSS 选择器支持的灵活性。CSS 语法通常用于帮助美化 HTML 页面,因此标准.css文件中的选择器使用 HTML 标签名称(如div和body)来定位内容。然而,jQuery 可以像标准 HTML 一样轻松地使用任意的 XML 标签名称,比如entry和definition。
jQuery 内部的高级选择器引擎使在更复杂的情况下找到 XML 文档的部分变得更加容易。例如,假设我们想将显示的条目限制为具有又带有作者的引用的条目。为此,我们可以通过将entry更改为entry:has(quote)来限制具有嵌套的<quote>元素的条目。然后,我们可以通过编写entry:has(quote[author])来进一步限制具有<quote>元素上的author属性的条目。现在,列表 6.9 中的带有初始选择器的行如下所示:
$(data).find('entry:has(quote[author])').each(function() {
这个新的选择器表达式相应地限制了返回的条目:
虽然我们可以在从服务器返回的 XML 数据上使用 jQuery,但缺点是我们的代码量已经显著增长。
选择数据格式
我们已经查看了四种用于外部数据的格式,每种格式都由 jQuery 的 Ajax 函数处理。我们还验证了所有四种格式都能够处理手头的任务,在用户请求时加载信息到现有页面上,并且在此之前不加载。那么,我们如何决定在我们的应用程序中使用哪种格式?
HTML 片段 需要非常少的工作来实现。可以使用一个简单的方法将外部数据加载并插入到页面中,甚至不需要回调函数。对于简单的任务,添加新的 HTML 到现有页面中不需要遍历数据。另一方面,数据的结构不一定适合其他应用程序重用。外部文件与其预期的容器紧密耦合。
JSON 文件 结构化简单,易于重用。它们紧凑且易于阅读。必须遍历数据结构以提取信息并在页面上呈现,但这可以通过标准 JavaScript 技术完成。由于现代浏览器可以通过单个调用JSON.parse()原生解析文件,读取 JSON 文件非常快速。JSON 文件中的错误可能导致静默失败,甚至在页面上产生副作用,因此数据必须由可信任的方进行精心制作。
JavaScript 文件 提供了最大的灵活性,但实际上并不是一种数据存储机制。由于文件是特定于语言的,因此无法用于向不同的系统提供相同的信息。相反,加载 JavaScript 文件的能力意味着很少需要的行为可以拆分到外部文件中,减少代码大小,直到需要为止。
尽管 XML 在 JavaScript 社区中已经不再受欢迎,大多数开发人员更喜欢 JSON,但它仍然如此普遍,以至于以此格式提供数据很可能使数据在其他地方得到重用。XML 格式有点臃肿,解析和操作速度可能比其他选项慢一些。
考虑到这些特点,通常最容易将外部数据提供为 HTML 片段,只要数据不需要在其他应用程序中使用。在数据将被重用但其他应用程序也可能受到影响的情况下,由于其性能和大小,JSON 通常是一个不错的选择。当远程应用程序未知时,XML 可能提供最大的保证,可以实现互操作性。
比起其他任何考虑因素,我们应确定数据是否已经可用。如果是,那么很可能最初就是以其中一种这种格式呈现的,因此决策可能已经为我们做出。
向服务器传递数据
到目前为止,我们的示例重点放在从 Web 服务器检索静态数据文件的任务上。但是,服务器可以根据来自浏览器的输入动态地塑造数据。在这项任务中,jQuery 也为我们提供了帮助;我们迄今为止介绍的所有方法都可以修改,以便数据传输变成双向街道。
与服务器端代码交互
由于演示这些技术需要与 Web 服务器进行交互,所以我们将在这里首次使用服务器端代码。给出的示例将使用 Node.js,它非常广泛使用并且免费提供。我们不会在这里涵盖任何 Node.js 或 Express 的具体内容,但是如果你搜索这两项技术,网络上有丰富的资源可供参考。
执行 GET 请求
为了说明客户端(使用 JavaScript)与服务器(同样使用 JavaScript)之间的通信,我们将编写一个脚本,每次请求只向浏览器发送一个词典条目。所选择的条目将取决于从浏览器发送的参数。我们的脚本将从类似于这样的内部数据结构中获取数据:
const E_entries = {
EAVESDROP: {
part: 'v.i.',
definition: 'Secretly to overhear a catalogue of the ' +
'crimes and vices of another or yourself.',
quote: [
'A lady with one of her ears applied',
'To an open keyhole heard, inside,',
'Two female gossips in converse free —',
'The subject engaging them was she.',
'"I think," said one, "and my husband thinks',
'That she's a prying, inquisitive minx!"',
'As soon as no more of it she could hear',
'The lady, indignant, removed her ear.',
'"I will not stay," she said, with a pout,',
'"To hear my character lied about!"',
],
author: 'Gopete Sherany',
},
EDIBLE: {
part:'adj.',
definition: 'Good to eat, and wholesome to digest, as ' +
'a worm to a toad, a toad to a snake, a snake ' +
'to a pig, a pig to a man, and a man to a worm.',
},
// Etc...
在这个示例的生产版本中,数据可能会存储在数据库中,并根据需要加载。由于数据在这里是脚本的一部分,所以检索它的代码非常简单。我们检查 URL 的查询字符串部分,然后将术语和条目传递给一个返回 HTML 片段以显示的函数:
const formatAuthor = entry =>
entry.author ?
`<div class="quote-author">${entry.author}</div>` :
'';
const formatQuote = entry =>
entry.quote ?
`
<div class="quote">
${entry.quote.reduce((result, q) => `
${result}
<div class="quote-line">${q}</div>
`, '')}
${formatAuthor(entry)}
</div>
` : '';
const formatEntry = (term, entry) => `
<div class="entry">
<h3 class="term">${term}</h3>
<div class="part">${entry.part}</div>
<div class="definition">
${entry.definition}
${formatQuote(entry)}
</div>
</div>
`;
app.use(express.static('./'));
app.get('/e', (req, res) => {
const term = req.query.term.toUpperCase();
const entry = E_entries[term];
let content;
if (entry) {
content = formatEntry(term, entry);
} else {
content = '<div>Sorry, your term was not found.</div>';
}
res.send(content);
});
现在,对这个 /e 处理器的请求,将返回对应于在 GET 参数中发送的术语的 HTML 片段。例如,当使用 /e?term=eavesdrop 访问处理器时,我们会得到:
再次注意我们之前看到的 HTML 片段缺乏格式,因为尚未应用 CSS 规则。
由于我们正在展示数据如何传递到服务器,所以我们将使用不同的方法来请求条目,而不是迄今为止所依赖的孤立按钮。相反,我们将为每个术语呈现一个链接列表,并且点击任何一个链接都将加载相应的定义。我们将添加以下 HTML:
<div class="letter" id="letter-e">
<h3>E</h3>
<ul>
<li><a href="e?term=Eavesdrop">Eavesdrop</a></li>
<li><a href="e?term=Edible">Edible</a></li>
<li><a href="e?term=Education">Education</a></li>
<li><a href="e?term=Eloquence">Eloquence</a></li>
<li><a href="e?term=Elysium">Elysium</a></li>
<li><a href="e?term=Emancipation">Emancipation</a>
</li>
<li><a href="e?term=Emotion">Emotion</a></li>
<li><a href="e?term=Envelope">Envelope</a></li>
<li><a href="e?term=Envy">Envy</a></li>
<li><a href="e?term=Epitaph">Epitaph</a></li>
<li><a href="e?term=Evangelist">Evangelist</a></li>
</ul>
</div>
现在,我们需要让我们的前端 JavaScript 代码调用后端 JavaScript,并传递正确的参数。我们可以使用正常的 .load() 机制来做到这一点,直接将查询字符串附加到 URL 并使用类似于 e?term=eavesdrop 的地址获取数据。但是,我们可以让 jQuery 根据我们提供给 $.get() 函数的对象构造查询字符串:
$(() => {
$('#letter-e a')
.click((e) => {
e.preventDefault();
const requestData = {
term: $(e.target).text()
};
$.get('e', requestData, (data) => {
$('#dictionary').html(data);
});
});
});
列表 6.10
现在我们已经看到 jQuery 提供的其他 Ajax 接口,这个函数的操作看起来很熟悉。唯一的区别是第二个参数,它允许我们提供一个包含键和值的对象,这些键和值成为查询字符串的一部分。在这种情况下,键始终是 term,但值是从每个链接的文本中获取的。现在,点击列表中的第一个链接会显示其定义:
这里的所有链接都有 URL,即使我们在代码中没有使用它们。为了防止链接在点击时正常跟随,我们调用.preventDefault()方法。
返回 false 还是阻止默认行为?
在本章中编写 click 处理程序时,我们选择使用 e.preventDefault() 而不是以 return false 结束处理程序。当默认操作否则会重新加载页面或加载另一页时,建议采用这种做法。例如,如果我们的 click 处理程序包含 JavaScript 错误,调用处理程序的第一行.preventDefault()(在遇到错误之前)确保表单不会被提交,并且我们浏览器的错误控制台将正确报告错误。请记住,从 第三章 处理事件,return false 调用了 event.preventDefault() 和 event.stopPropagation()。如果我们想要阻止事件冒泡,我们还需要调用后者。
序列化表单
将数据发送到服务器通常涉及用户填写表单。与其依赖于正常的表单提交机制,该机制将在整个浏览器窗口中加载响应,我们可以使用 jQuery 的 Ajax 工具包异步提交表单并将响应放置在当前页面中。
要尝试这个,我们需要构建一个简单的表单:
<div class="letter" id="letter-f">
<h3>F</h3>
<form action="f">
<input type="text" name="term" value="" id="term" />
<input type="submit" name="search" value="search"
id="search" />
</form>
</div>
这一次,我们将通过使我们的 /f 处理程序搜索提供的搜索词作为字典词的子字符串来从服务器返回一组条目。我们将使用我们从 /e 处理程序 中的 formatEntry() 函数以与之前相同的格式返回数据。以下是 /f 处理程序的实现:
app.post('/f', (req, res) => {
const term = req.body.term.toUpperCase();
const content = Object.keys(F_entries)
.filter(k => k.includes(term))
.reduce((result, k) => `
${result}
${formatEntry(k, F_entries[k])}
`, '');
res.send(content);
});
现在,我们可以对表单提交做出反应,并通过遍历 DOM 树来制作正确的查询参数:
$(() => {
$('#letter-f form')
.submit((e) => {
e.preventDefault();
$.post(
$(e.target).attr('action'),
{ term: $('input[name="term"]').val() },
(data) => { $('#dictionary').html(data); }
);
});
});
清单 6.11
此代码具有预期效果,但按名称搜索输入字段并逐个将其附加到地图中是繁琐的。特别是,随着表单变得更加复杂,这种方法的扩展性不佳。幸运的是,jQuery 提供了一个经常使用的惯用语的快捷方式。.serialize() 方法作用于 jQuery 对象,并将匹配的 DOM 元素转换为可以与 Ajax 请求一起传递的查询字符串。我们可以将我们的提交处理程序概括如下:
$(() => {
$('#letter-f form')
.submit((e) => {
e.preventDefault();
$.post(
$(e.target).attr('action'),
$(e.target).serialize(),
(data) => { $('#dictionary').html(data); }
);
});
});
清单 6.12
同样的脚本将用于提交表单,即使字段数量增加。例如,当我们搜索 fid 时,包含该子字符串的术语会显示如下屏幕截图所示:
注意请求
到目前为止,我们只需调用一个 Ajax 方法并耐心等待响应就足够了。然而,有时候,了解 HTTP 请求在进行中的情况会很方便。如果出现这种需要,jQuery 提供了一套函数,可以在发生各种与 Ajax 相关的事件时注册回调函数。
.ajaxStart() 和 .ajaxStop() 方法是这些观察者函数的两个示例。当没有其他传输正在进行时开始一个 Ajax 调用时,将触发 .ajaxStart() 回调。相反,当最后一个活动请求结束时,将执行与 .ajaxStop() 绑定的回调。所有观察者都是全局的,它们在发生任何 Ajax 通信时被调用,无论是什么代码启动的。而且所有这些观察者只能绑定到 $(document)。
我们可以利用这些方法在网络连接缓慢的情况下向用户提供一些反馈。页面的 HTML 可以附加适当的加载消息:
<div id="loading">
Loading...
</div>
这个消息只是一段任意的 HTML 代码;例如,它可以包含一个动画 GIF 图像作为加载指示器。在这种情况下,我们将在 CSS 文件中添加一些简单的样式,以便在显示消息时,页面看起来如下:
为了符合渐进增强的精神,我们不会直接将这个 HTML 标记放在页面上。相反,我们将使用 jQuery 插入它:
$(() => {
$('<div/>')
.attr('id', 'loading')
.text('Loading...')
.insertBefore('#dictionary');
});
我们的 CSS 文件将给这个 <div> 添加一个 display: none; 的样式声明,以便最初隐藏消息。在适当的时候显示它,我们只需使用 .ajaxStart() 将其注册为观察者:
$(() => {
const $loading = $('<div/>')
.attr('id', 'loading')
.text('Loading...')
.insertBefore('#dictionary');
$(document)
.ajaxStart(() => {
$loading.show();
});
});
我们可以将隐藏行为链接在一起:
$(() => {
const $loading = $('<div/>')
.attr('id', 'loading')
.text('Loading...')
.insertBefore('#dictionary');
$(document)
.ajaxStart(() => {
$loading.show();
})
.ajaxStop(() => {
$loading.hide();
});
});
列表 6.13
现在我们有了加载反馈。
再次说明,这些方法与 Ajax 通信开始的具体方式无关。附加到 A 链接的 .load() 方法和附加到 B 链接的 .getJSON() 方法都会导致这些操作发生。
在这种情况下,这种全局行为是可取的。不过,如果我们需要更具体的行为,我们有几个选择。一些观察者方法,比如 .ajaxError(),会将它们的回调函数发送给 XMLHttpRequest 对象的引用。这可以用于区分一个请求和另一个请求,并提供不同的行为。通过使用低级别的 $.ajax() 函数,我们可以实现其他更具体的处理,稍后我们会讨论这个函数。
与请求交互的最常见方式是 success 回调,我们已经介绍过了。我们在几个示例中使用它来解释从服务器返回的数据,并用结果填充页面。当然,它也可以用于其他反馈。再次考虑我们从 列表 6.1 中的 .load() 示例:
$(() => {
$('#letter-a a')
.click((e) => {
e.preventDefault();
$('#dictionary')
.load('a.html');
});
});
我们可以通过使加载的内容淡入而不是突然出现来进行一点小改进。.load() 方法可以接受一个回调函数在完成时被触发:
$(() => {
$('#letter-a a')
.click((e) => {
e.preventDefault();
$('#dictionary')
.hide()
.load('a.html', function() {
$(this).fadeIn();
});
});
});
列表 6.14
首先,我们隐藏目标元素,然后开始加载。加载完成后,我们使用回调函数将新填充的元素显示出来,以淡入的方式。
错误处理
到目前为止,我们只处理了 Ajax 请求的成功响应,当一切顺利时加载页面以显示新内容。然而,负责任的开发人员应考虑网络或数据错误的可能性,并适当地报告它们。在本地环境中开发 Ajax 应用程序可能会使开发人员产生自满感,因为除了可能的 URL 输入错误外,Ajax 错误不会在本地发生。Ajax 方便的方法,如$.get()和.load()本身不提供错误回调参数,因此我们需要寻找其他地方解决此问题。
除了使用global .ajaxError()方法外,我们还可以利用 jQuery 的延迟对象系统来对错误做出反应。我们将在第十一章,高级效果中更详细地讨论延迟对象,但是,现在我们简单地指出,我们可以将.done(),.always()和.fail()方法链接到除.load()之外的任何 Ajax 函数,并使用这些方法来附加相关的回调。例如,如果我们取自列表 6.16的代码,并将 URL 更改为不存在的 URL,我们可以测试.fail()方法:
$(() => {
$('#letter-e a')
.click((e) => {
e.preventDefault();
const requestData = {
term: $(e.target).text()
};
$.get('notfound', requestData, (data) => {
$('#dictionary').html(data);
}).fail((xhr) => {
$('#dictionary')
.html(`An error occurred:
${xhr.status}
${xhr.responseText}
`);
});
});
});
列表 6.15
现在,点击以 E 开头的任何术语链接都会产生错误消息。jqXHR.responseText的确切内容将根据服务器配置的不同而变化:
.status属性包含服务器提供的数字代码。这些代码在 HTTP 规范中定义,当触发.fail()处理程序时,它们将代表错误条件,例如:
| 响应代码 | 描述 |
|---|---|
| 400 | 错误请求 |
| 401 | 未经授权 |
| 403 | 禁止访问 |
| 404 | 未找到 |
| 500 | 内部服务器错误 |
可以在 W3C 的网站上找到完整的响应代码列表:www.w3.org/Protocols/rfc2616/rfc2616-sec10.html。
我们将更仔细地检查错误处理在第十三章,高级 Ajax中。
Ajax 和事件
假设我们想允许每个词典术语名称控制其后跟的定义的显示;点击术语名称将显示或隐藏相关定义。根据我们目前所见的技术,这应该是相当简单的:
$(() => {
$('h3.term')
.click((e) => {
$(e.target)
.siblings('.definition')
.slideToggle();
});
});
列表 6.16
当点击术语时,此代码会查找具有definition类的元素的兄弟元素,并根据需要将它们上下滑动。
一切看起来都井然有序,但此代码不起作用。不幸的是,当我们附加click处理程序时,术语尚未添加到文档中。即使我们设法将click处理程序附加到这些项上,一旦我们点击不同的字母,处理程序将不再附加。
这是一个常见的问题,当页面的某些区域由 Ajax 填充时。一个流行的解决方案是每次页面区域被刷新时重新绑定处理程序。然而,这可能很麻烦,因为每次任何事情导致页面的 DOM 结构发生变化时,事件绑定代码都需要被调用。
更优的选择在第三章,事件处理中被介绍。我们可以实现事件委托,实际上将事件绑定到一个永远不会改变的祖先元素上。在这种情况下,我们将click处理程序附加到<body>元素上,使用.on()这样来捕获我们的点击:
$(() => {
$('body')
.on('click', 'h3.term', (e) => {
$(e.target)
.siblings('.definition')
.slideToggle();
});
});
第 6.17 节
当以这种方式使用时,.on()方法告诉浏览器在整个文档中观察所有点击。如果(且仅当)点击的元素与h3.term选择器匹配,则执行处理程序。现在,切换行为将在任何术语上发生,即使它是由后来的 Ajax 事务添加的。
延迟对象和承诺
在 JavaScript 代码中处理异步行为时,jQuery 延迟对象是在没有一致的方式时引入的。承诺帮助我们编排异步事务,如多个 HTTP 请求、文件读取、动画等。承诺不是 JavaScript 专有的,也不是一个新的想法。将承诺视为一个承诺最终解析值的合同是最好的理解方式。
现在承诺已经正式成为 JavaScript 的一部分,jQuery 现在完全支持承诺。也就是说,jQuery 延迟对象的行为与任何其他承诺一样。这很重要,因为我们将在本节中看到,这意味着我们可以使用 jQuery 延迟对象来与返回原生承诺的其他代码组合复杂的异步行为。
在页面加载时执行 Ajax 调用
现在,我们的字典在初始页面加载时不显示任何定义。相反,它只显示一些空白空间。让我们通过在文档准备好时显示"A"条目来改变这种情况。我们如何做到这一点?
一种方法是简单地将load('a.html')调用添加到我们的文档准备处理程序($(() => {}))中,以及其他所有内容。问题在于这样效率低下,因为我们必须等待文档准备好才能发出 Ajax 请求。如果我们的 JavaScript 一运行就发出 Ajax 请求会不会更好呢?
挑战在于将文档准备事件与 Ajax 响应准备事件同步。这里存在竞争条件,因为我们不知道哪个事件会先发生。文档准备可能会首先完成,但我们不能做出这种假设。这就是承诺非常有用的地方:
Promise.all([
$.get('a.html'),
$.ready
]).then(([content]) => {
$('#dictionary')
.hide()
.html(content)
.fadeIn();
});
第 6.18 节
Promise.all()方法接受其他 promise 的数组,并返回一个新的 promise。当数组参数中的所有内容都解析了,这个新的 promise 就解析了。这就是 promise 为我们处理异步竞争条件的方式。无论 Ajax promise ($.get('a.html'))先解析还是文档准备好 promise ($.ready)先解析,都不重要。
then()处理程序是我们想要执行依赖于异步值的任何代码的地方。例如,content 值是解析后的 Ajax 调用。文档准备好隐式解析了 DOM。如果 DOM 没有准备好,我们就不能运行$('#dictionary')...。
使用 fetch()
JavaScript 的另一个近期新增功能是fetch()函数。这是XMLHttpRequest的更灵活的替代品。例如,当进行跨域请求时或需要调整特定的 HTTP 头值时,使用fetch()更加容易。让我们使用fetch()来实现G条目:
$(() => {
$('#letter-g a')
.click((e) => {
e.preventDefault();
fetch('/g')
.then(resp => resp.json())
.then(data => {
const html = data.reduce((result, entry) => `
${result}
<div class="entry">
<h3 class="term">${entry.term}</h3>
<div class="part">${entry.part}</div>
<div class="definition">
${entry.definition}
${formatQuote(entry)}
</div>
</div>
`, '');
$('#dictionary')
.html(html);
});
});
});
列表 6.19
fetch()函数返回一个 promise,就像各种 jQuery Ajax 函数一样。这意味着如果我们在这个例子中调用的/g网址实际上位于另一个域中,我们可以使用fetch()来访问它。如果我们需要 JSON 数据,我们需要在.then()处理程序中调用.json()。然后,在第二个处理程序中,我们可以使用在本章前面创建的相同函数来填充 DOM。
Promise 背后的整个理念是一致性。如果我们需要同步异步行为,promise 是解决的方法。任何 jQuery 异步执行的内容,都可以使用其他 promise。
总结
你已经学会了 jQuery 提供的 Ajax 方法可以帮助我们从服务器加载多种不同格式的数据,而无需页面刷新。我们可以根据需要从服务器执行脚本,并将数据发送回服务器。
你还学会了如何处理异步加载技术的常见挑战,比如在加载完成后保持处理程序的绑定以及从第三方服务器加载数据。
这结束了我们对jQuery库基本组件的介绍。接下来,我们将看看这些功能如何通过 jQuery 插件轻松扩展。
进一步阅读
Ajax 的主题将在第十三章 高级 Ajax中更详细地探讨。完整的 Ajax 方法列表可以在本书的附录 B 快速参考或官方的 jQuery 文档中找到 api.jquery.com/。
练习
挑战性的练习可能需要使用官方的 jQuery 文档
-
当页面加载时,将
exercises-content.html的内容填充到页面的内容区域。 -
而不是一次性显示整个文档,当用户将鼠标悬停在左侧列中的字母上时,通过从
exercises-content.html加载适当字母的内容,创建工具提示。 -
为这个页面加载添加错误处理,将错误消息显示在内容区域。通过将脚本更改为请求
does-not-exist.html而不是exercises-content.html来测试这个错误处理代码。 -
这是一个挑战。页面加载时,向 GitHub 发送一个 JSONP 请求,并检索用户的存储库列表。将每个存储库的名称和网址插入页面的内容区域。检索 jQuery 项目存储库的网址是
api.github.com/users/jquery/repos。
第七章:使用插件
在本书的前六章中,我们审视了 jQuery 的核心组件。这样做已经说明了 jQuery 库可以用来完成各种任务的许多方法。尽管库在其核心处非常强大,但其优雅的插件架构使开发人员能够扩展 jQuery,使其功能更加丰富。
jQuery 社区创建了数百个插件——从小的选择器辅助工具到完整的用户界面部件。现在,您将学习如何利用这一庞大资源。
在本章中,我们将介绍:
-
下载和设置插件
-
调用插件提供的 jQuery 方法
-
使用由 jQuery 插件定义的自定义选择器查找元素
-
使用 jQuery UI 添加复杂的用户界面行为
-
使用 jQuery Mobile 实现移动友好功能
使用插件
使用 jQuery 插件非常简单。我们只需要获取插件代码,从我们的 HTML 中引用插件,并从我们自己的脚本中调用新的功能。
我们可以使用 jQuery Cycle 插件轻松演示这些任务。这个由 Mike Alsup 制作的插件可以快速地将静态页面元素集合转换为交互式幻灯片。像许多流行的插件一样,它可以很好地处理复杂的高级需求,但当我们的需求更为简单时,它也可以隐藏这种复杂性。
下载并引用 Cycle 插件
要安装任何 jQuery 插件,我们将使用 npm 包管理器。这是声明现代 JavaScript 项目的包依赖关系的事实上的工具。例如,我们可以使用 package.json 文件声明我们需要 jQuery 和一组特定的 jQuery 插件。
要获取有关安装 npm 的帮助,请参阅 docs.npmjs.com/getting-started/what-is-npm。要获取有关初始化 package.json 文件的帮助,请参阅 docs.npmjs.com/getting-started/using-a-package.json。
一旦在项目目录的根目录中有了 package.json 文件,您就可以开始添加依赖项了。例如,您可以从命令控制台如下添加 jquery 依赖项:
npm install jquery --save
如果我们想要使用 cycle 插件,我们也可以安装它:
npm install jquery-cycle --save
我们在此命令中使用 --save 标志的原因是告诉 npm 我们始终需要这些包,并且它应该将这些依赖项保存到 package.json。现在我们已经安装了 jquery 和 jquery-cycle,让我们将它们包含到我们的页面中:
<head>
<meta charset="utf-8">
<title>jQuery Book Browser</title>
<link rel="stylesheet" href="07.css" type="text/css" />
<script src="img/jquery.js"></script>
<script src="img/index.js"></script>
<script src="img/07.js"></script>
</head>
我们现在已经加载了我们的第一个插件。正如我们所看到的,这不再比设置 jQuery 本身更复杂。插件的功能现在可以在我们的脚本中使用了。
调用插件方法
Cycle 插件可以作用于页面上的任何一组兄弟元素。为了展示它的运行过程,我们将设置一些简单的 HTML,其中包含书籍封面图像和相关信息的列表,并将其添加到我们 HTML 文档的主体中,如下所示:
<ul id="books">
<li>
<img src="img/jq-game.jpg" alt="jQuery Game Development
Essentials" />
<div class="title">jQuery Game Development Essentials</div>
<div class="author">Salim Arsever</div>
</li>
<li>
<img src="img/jqmobile-cookbook.jpg" alt="jQuery Mobile
Cookbook" />
<div class="title">jQuery Mobile Cookbook</div>
<div class="author">Chetan K Jain</div>
</li>
...
</ul>
在我们的 CSS 文件中进行轻量级样式处理,按照以下截图所示,依次显示书籍封面:
Cycle 插件将会在此列表上发挥其魔力,将其转换为一个引人注目的动画幻灯片。通过在 DOM 中适当的容器上调用 .cycle() 方法,可以调用此转换,如下所示:
$(() => {
$('#books').cycle();
});
列表 7.1
这种语法几乎没有更简单的了。就像我们使用任何内置的 jQuery 方法一样,我们将 .cycle() 应用于一个 jQuery 对象实例,该实例又指向我们要操作的 DOM 元素。即使没有向它提供任何参数,.cycle() 也为我们做了很多工作。页面上的样式被修改以仅呈现一个列表项,并且每 4 秒使用淡入淡出的转换显示一个新项:
这种简单性是写得很好的 jQuery 插件的典型特征。只需简单的方法调用就能实现专业且有用的结果。然而,像许多其他插件一样,Cycle 提供了大量的选项,用于定制和微调其行为。
指定插件方法参数
将参数传递给插件方法与使用原生 jQuery 方法没有什么不同。在许多情况下,参数被传递为一个键值对的单个对象(就像我们在第六章中看到的 $.ajax(),使用 Ajax 发送数据)。提供的选项选择可能会令人生畏;.cycle() 本身就有超过 50 个潜在的配置选项。每个插件的文档详细说明了每个选项的效果,通常还附有详细的示例。
Cycle 插件允许我们改变幻灯片之间的动画速度和样式,影响何时以及如何触发幻灯片转换,并使用回调函数来响应完成的动画。为了演示其中一些功能,我们将从 列表 7.1 的方法调用中提供三个简单的选项,如下所示:
$(() => {
$('#books').cycle({
timeout: 2000,
speed: 200,
pause: true
});
});
列表 7.2
timeout 选项指定了在每个幻灯片转换之间等待的毫秒数(2,000)。相比之下,speed 决定了转换本身需要花费的毫秒数(200)。当设置为 true 时,pause 选项会导致幻灯片秀在鼠标位于循环区域内时暂停,当循环项可点击时尤其有用。
修改参数默认值
即使没有提供参数,Cycle 插件也是令人印象深刻的。为了实现这一点,当没有提供选项时,它需要一个合理的默认设置来使用。
一种常见的模式,也是 Cycle 遵循的模式,是将所有默认值收集到一个单一对象中。在 Cycle 的情况下,$.fn.cycle.defaults 对象包含所有默认选项。当插件将其默认值收集在像这样的公开可见位置时,我们可以在我们自己的脚本中修改它们。这样可以使我们的代码在多次调用插件时更加简明,因为我们不必每次都指定选项的新值。重新定义默认值很简单,如下面的代码所示:
$.fn.cycle.defaults.timeout = 10000;
$.fn.cycle.defaults.random = true;
$(() => {
$('#books').cycle({
timeout: 2000,
speed: 200,
pause: true
});
});
列表 7.3
在这里,我们在调用.cycle() 之前设置了两个默认值,timeout 和 random。由于我们在.cycle() 中声明了 timeout 的值为 2000,我们的新默认值 10000 会被忽略。另一方面,random 的新默认值为 true 生效,导致幻灯片以随机顺序过渡。
其他类型的插件
插件不仅仅限于提供额外的 jQuery 方法。它们可以在许多方面扩展库甚至改变现有功能的功能。
插件可以改变 jQuery 库的其他部分操作的方式。例如,一些插件提供新的动画缓动样式,或者在用户操作响应中触发额外的 jQuery 事件。Cycle 插件通过添加一个新的自定义选择器来提供这样的增强功能。
自定义选择器
添加自定义选择器表达式的插件会增加 jQuery 内置选择器引擎的功能,使我们可以以新的方式在页面上查找元素。Cycle 添加了这种类型的自定义选择器,这给了我们一个探索这种功能的机会。
通过调用.cycle('pause') 和 .cycle('resume'),Cycle 的幻灯片可以暂停和恢复。我们可以轻松地添加控制幻灯片的按钮,如下面的代码所示:
$(() => {
const $books = $('#books').cycle({
timeout: 2000,
speed: 200,
pause: true
});
const $controls = $('<div/>')
.attr('id', 'books-controls')
.insertAfter($books);
$('<button/>')
.text('Pause')
.click(() => {
$books.cycle('pause');
})
.appendTo($controls);
$('<button/>')
.text('Resume')
.click(() => {
$books.cycle('resume');
})
.appendTo($controls);
});
列表 7.4
现在,假设我们希望我们的“恢复”按钮恢复页面上任何暂停的 Cycle 幻灯片,如果有多个的话。我们想要找到页面上所有暂停的幻灯片的<ul> 元素,并恢复它们所有。Cycle 的自定义:paused 选择器使我们可以轻松做到这一点:
$(() => {
$('<button/>')
.text('Resume')
.click(() => {
$('ul:paused').cycle('resume');
})
.appendTo($controls);
});
列表 7.5
使用 Cycle 加载,$('ul:paused') 将创建一个 jQuery 对象,引用页面上所有暂停的幻灯片,以便我们可以随意进行交互。像这样由插件提供的选择器扩展可以自由地与任何标准的 jQuery 选择器结合使用。我们可以看到,选择适当的插件,jQuery 可以被塑造以满足我们的需求。
全局函数插件
许多流行的插件在jQuery命名空间中提供新的全局函数。当插件提供的功能与页面上的 DOM 元素无关,因此不适合标准的 jQuery 方法时,这种模式是常见的。例如,Cookie 插件(github.com/carhartl/jquery-cookie)提供了一个界面,用于在页面上读取和写入 cookie 值。这个功能是通过$.cookie()函数提供的,它可以获取或设置单个 cookie。
比如说,例如,我们想要记住用户什么时候按下我们幻灯片的暂停按钮,以便如果他们离开页面然后过后回来的话我们可以保持它暂停。加载 Cookie 插件之后,读取 cookie 就像在下面代码中一样简单:只需将 cookie 的名称作为唯一参数使用即可。
if ($.cookie('cyclePaused')) {
$books.cycle('pause');
}
列表 7.6
在这里,我们寻找cyclePaused cookie 的存在;对于我们的目的来说,值是无关紧要的。如果 cookie 存在,循环将暂停。当我们在调用.cycle()之后立即插入这个条件暂停时,幻灯片会一直保持第一张图片可见,直到用户在某个时候按下“恢复”按钮。
当然,因为我们还没有设置 cookie,幻灯片仍在循环播放图片。设置 cookie 和获取它的值一样简单;我们只需像下面这样为第二个参数提供一个字符串:
$(() => {
$('<button/>')
.text('Pause')
.click(() => {
$books.cycle('pause');
$.cookie('cyclePaused', 'y');
})
.appendTo($controls);
$('<button/>')
.text('Resume')
.click(() => {
$('ul:paused').cycle('resume');
$.cookie('cyclePaused', null);
})
.appendTo($controls);
});
列表 7.7
当按下暂停按钮时,cookie 被设置为y,当按下恢复按钮时,通过传递null来删除 cookie。默认情况下,cookie 在会话期间保持(通常直到浏览器标签页关闭)。同样默认情况下,cookie 与设置它的页面相关联。要更改这些默认设置,我们可以为函数的第三个参数提供一个选项对象。这是典型的 jQuery 插件模式,也是 jQuery 核心函数。
例如,为了使 cookie 在整个站点上可用,并在 7 天后过期,我们可以调用$.cookie('cyclePaused', 'y', { path: '/', expires: 7 })。要了解在调用$.cookie()时可用的这些和其他选项的信息,我们可以参考插件的文档。
jQuery UI 插件库
虽然大多数插件,比如 Cycle 和 Cookie,都专注于一个单一的任务,jQuery UI 却面对着各种各样的挑战。实际上,虽然 jQuery UI 的代码常常被打包成一个单一的文件,但它实际上是一套相关插件的综合套件。
jQuery UI 团队创建了许多核心交互组件和成熟的小部件,以帮助使网络体验更像桌面应用程序。交互组件包括拖放、排序、选择和调整项的方法。目前稳定的小部件包括按钮、手风琴、日期选择器、对话框等。此外,jQuery UI 还提供了一套广泛的高级效果,以补充核心的 jQuery 动画。
完整的 UI 库过于庞大,无法在本章中充分覆盖;事实上,有整本书专门讨论此主题。幸运的是,该项目的主要焦点是其功能之间的一致性,因此详细探讨几个部分将有助于我们开始使用其余的部分。
所有 jQuery UI 模块的下载、文档和演示都可以在此处找到
jqueryui.com/。下载页面提供了一个包含所有功能的组合下载,或者一个可定制的下载,可以包含我们需要的功能。可下载的 ZIP 文件还包含样式表和图片,我们在使用 jQuery UI 的交互组件和小部件时需要包含它们。
效果
jQuery UI 的效果模块由核心和一组独立的效果组件组成。核心文件提供了颜色和类的动画,以及高级缓动。
颜色动画
将 jQuery UI 的核心效果组件链接到文档中后,.animate() 方法被扩展以接受额外的样式属性,例如 borderTopColor、backgroundColor 和 color。例如,我们现在可以逐渐将元素从黑色背景上的白色文本变为浅灰色背景上的黑色文本:
$(() => {
$books.hover((e) => {
$(e.target)
.find('.title')
.animate({
backgroundColor: '#eee',
color: '#000'
}, 1000);
}, (e) => {
$(e.target)
.find('.title')
.animate({
backgroundColor: '#000',
color: '#fff'
}, 1000);
});
});
清单 7.8
现在,当鼠标光标进入页面的书籍幻灯片区域时,书名的文本颜色和背景颜色都会在一秒钟(1000 毫秒)的时间内平滑动画过渡:
类动画
我们在前几章中使用过的三个 CSS 类方法--.addClass()、.removeClass() 和 .toggleClass()--被 jQuery UI 扩展为接受可选的第二个参数,用于动画持续时间。当指定了这个持续时间时,页面的行为就像我们调用了 .animate(),并直接指定了应用于元素的类的所有样式属性变化一样:
$(() => {
$('h1')
.click((e) => {
$(e.target).toggleClass('highlighted', 'slow');
});
});
清单 7.9
通过执行 清单 7.9 中的代码,我们已经导致页面标题的点击添加或删除 highlighted 类。但是,由于我们指定了 slow 速度,结果的颜色、边框和边距变化会以动画形式展现出来,而不是立即生效:
高级缓动
当我们指示 jQuery 在指定的持续时间内执行动画时,它并不是以恒定的速率执行。例如,如果我们调用 $('#my-div').slideUp(1000),我们知道元素的高度将需要整整一秒钟才能达到零;但是,在该秒的开始和结束时,高度将缓慢变化,在中间时将快速变化。这种速率变化被称为缓动,有助于动画看起来平滑自然。
高级缓动函数变化加速和减速曲线,以提供独特的结果。例如,easeInExpo函数呈指数增长,以多倍于开始时的速度结束动画。我们可以在任何核心 jQuery 动画方法或 jQuery UI 效果方法中指定自定义缓动函数。这可以通过添加参数或将选项添加到设置对象中来完成,具体取决于使用的语法。
要查看此示例,请按照以下方式将easeInExpo作为我们刚刚介绍的第 7.9 部分中的.toggleClass()方法的缓动样式提供:
$(() => {
$('h1')
.click((e) => {
$(e.target)
.toggleClass(
'highlighted',
'slow',
'easeInExpo'
);
});
});
第 7.10 部分
现在,每当单击标题时,通过切换类属性修改的样式都会逐渐出现,然后加速并突然完成过渡。
查看缓动函数的效果
完整的缓动函数集合演示可在
其他效果
包含在 jQuery UI 中的单独效果文件添加了各种转换,其中一些可以比 jQuery 本身提供的简单滑动和淡出动画复杂得多。通过调用由 jQuery UI 添加的.effect()方法来调用这些效果。如果需要,可以使用.show()或.hide()来调用导致元素隐藏或显示的效果。
jQuery UI 提供的效果可以用于多种用途。其中一些,比如transfer和size,在元素改变形状和位置时非常有用。另一些,比如explode和puff,提供了吸引人的隐藏动画。还有一些,包括pulsate和shake,则将注意力吸引到元素上。
查看效果的实际效果
所有 jQuery UI 效果都在jqueryui.com/effect/#default展示。
shake行为特别适合强调当前不适用的操作。当简历按钮无效时,我们可以在页面上使用这个效果:
$(() => {
$('<button/>')
.text('Resume')
.click((e) => {
const $paused = $('ul:paused');
if ($paused.length) {
$paused.cycle('resume');
$.cookie('cyclePaused', null);
} else {
$(e.target)
.effect('shake', {
distance: 10
});
}
})
.appendTo($controls);
});
第 7.11 部分
我们的新代码检查$('ul:paused')的长度,以确定是否有任何暂停的幻灯片秀要恢复。如果是,则像以前一样调用 Cycle 的resume操作;否则,执行shake效果。在这里,我们看到,与其他效果一样,shake有可用于调整其外观的选项。在这里,我们将效果的distance设置为比默认值小的数字,以使按钮在点击时快速来回摇摆。
交互组件
jQuery UI 的下一个主要功能是其交互组件,这是一组行为,可以用来制作复杂的交互式应用程序。例如,其中一个组件是Resizable,它可以允许用户使用自然的拖动动作改变任何元素的大小。
对元素应用交互就像调用带有其名称的方法一样简单。例如,我们可以通过调用.resizable()来使书名可调整大小,如下所示:
(() => {
$books
.find('.title')
.resizable();
});
列表 7.12
在文档中引用了 jQuery UI 的 CSS 文件后,此代码将在标题框的右下角添加一个调整大小的手柄。拖动此框会改变区域的宽度和高度,如下面的截图所示:
正如我们现在可能期望的那样,这些方法可以使用大量选项进行定制。例如,如果我们希望将调整大小限制为仅在垂直方向上发生,我们可以通过指定应添加哪个拖动手柄来实现如下:
$(() => {
$books
.find('.title')
.resizable({ handles: 's' });
});
列表 7.13
只在区域的南(底部)侧有一个拖动手柄,只能改变区域的高度:
其他交互组件
其他 jQuery UI 交互包括可拖动的、可投放的和可排序的。与可调整大小一样,它们是高度可配置的。我们可以在jqueryui.com/上查看它们的演示和配置选项。
小部件
除了这些基本交互组件外,jQuery UI 还包括一些强大的用户界面小部件,它们的外观和功能像桌面应用程序中我们习惯看到的成熟元素一样。其中一些非常简单。例如,按钮小部件通过吸引人的样式和悬停状态增强了页面上的按钮和链接。
将此外观和行为授予页面上的所有按钮元素非常简单:
$(() => {
$('button').button();
});
列表 7.14
当引用 jQuery UI 平滑主题的样式表时,按钮将具有光滑、有倾斜的外观:
与其他 UI 小部件和交互一样,按钮接受几个选项。例如,我们可能希望为我们的两个按钮提供适当的图标;按钮小部件带有大量预定义的图标供我们使用。为此,我们可以将我们的.button()调用分成两部分,并分别指定每个图标,如下所示:
$(() => {
$('<button/>')
.text('Pause')
.button({
icons: { primary: 'ui-icon-pause' }
})
.click(() => {
// ...
})
.appendTo($controls);
$('<button/>')
.text('Resume')
.button({
icons: { primary: 'ui-icon-play' }
})
.click((e) => {
// ...
})
.appendTo($controls);
});
列表 7.15
我们指定的primary图标对应于 jQuery UI 主题框架中的标准类名。默认情况下,primary图标显示在按钮文本的左侧,而secondary图标显示在右侧:
另一方面,其他小部件要复杂得多。滑块小部件引入了一个全新的表单元素,类似于 HTML5 的范围元素,但与所有流行的浏览器兼容。这支持更高程度的自定义,如下面的代码所示:
$(() => {
$('<div/>')
.attr('id', 'slider')
.slider({
min: 0,
max: $books.find('li').length - 1
})
.appendTo($controls);
});
列表 7.16
对.slider()的调用将一个简单的<div>元素转换为滑块小部件。该小部件可以通过拖动或按箭头键来控制,以帮助实现可访问性:
在 清单 7.16 中,我们为滑块指定了一个最小值 0,并为幻灯片展示中的最后一本书的索引设置了最大值。我们可以将这个作为幻灯片的手动控制,通过在它们各自的状态改变时在幻灯片和滑块之间发送消息。
为了对滑块值的变化做出反应,我们可以将处理程序绑定到由滑块触发的自定义事件上。这个事件,slide,不是一个原生的 JavaScript 事件,但在我们的 jQuery 代码中表现得像一个。然而,观察这些事件是如此常见,以至于我们可以不需要显式地调用 .on(),而是可以直接将我们的事件处理程序添加到 .slider() 调用本身,如下面的代码所示:
$(() => {
$('<div/>')
.attr('id', 'slider')
.slider({
min: 0,
max: $books.find('li').length - 1,
slide: (e, ui) => {
$books.cycle(ui.value);
}
})
.appendTo($controls);
});
清单 7.17
每当调用 slide 回调时,它的 ui 参数就会填充有关小部件的信息,包括其当前值。通过将这个值传递给 Cycle 插件,我们可以操作当前显示的幻灯片。
我们还需要在幻灯片向前切换到另一个幻灯片时更新滑块小部件。为了在这个方向上进行通信,我们可以使用 Cycle 的 before 回调,在每次幻灯片转换之前触发:
$(() => {
const $books = $('#books').cycle({
timeout: 2000,
speed: 200,
pause: true,
before: (li) => {
$('#slider')
.slider(
'value',
$('#books li').index(li)
);
}
});
});
清单 7.18
在 before 回调中,我们再次调用 .slider() 方法。这一次,我们将 value 作为它的第一个参数调用,以设置新的滑块值。在 jQuery UI 的术语中,我们将 value 称为滑块的 方法,尽管它是通过调用 .slider() 方法而不是通过自己的专用方法名称来调用的。
其他小部件
其他 jQuery UI 小部件包括 Datepicker、Dialog、Tabs 和 Accordion。每个小部件都有几个相关的选项、事件和方法。完整列表,请访问
jQuery UI ThemeRoller
jQuery UI 库最令人兴奋的功能之一是 ThemeRoller,这是一个基于 Web 的交互式主题引擎,用于 UI 小部件。ThemeRoller 使得创建高度定制、专业外观的元素变得快速简单。我们刚刚创建的按钮和滑块都应用了默认主题;如果没有提供自定义设置,这个主题将从 ThemeRoller 输出:
生成完全不同风格的样式只需简单访问
jqueryui.com/themeroller/,根据需要修改各种选项,然后按下下载主题按钮。然后,可以将样式表和图像的 .zip 文件解压缩到您的站点目录中。例如,通过选择几种不同的颜色和纹理,我们可以在几分钟内为我们的按钮、图标和滑块创建一个新的协调外观,如下面的屏幕截图所示:
jQuery Mobile 插件库
我们已经看到 jQuery UI 如何帮助我们组装即使是复杂 web 应用程序所需的用户界面特性。它克服的挑战是多样且复杂的。然而,当为移动设备设计我们的页面以进行优雅的呈现和交互时,存在一组不同的障碍。为了创建现代智能手机和平板电脑的网站或应用程序,我们可以转向 jQuery Mobile 项目。
与 jQuery UI 一样,jQuery Mobile 由一套相关组件组成,可以单独使用,但可以无缝地一起工作。该框架提供了一个基于 Ajax 的导航系统、移动优化的交互元素和高级触摸事件处理程序。与 jQuery UI 一样,探索 jQuery Mobile 的所有功能是一个艰巨的任务,因此我们将提供一些简单的示例,并参考官方文档了解更多细节。
jQuery Mobile 的下载、文档和演示可在以下位置找到:
我们的 jQuery Mobile 示例将使用 Ajax 技术,因此需要网页服务器软件才能尝试这些示例。更多信息可在第六章中找到,使用 Ajax 发送数据。
HTML5 自定义 data 属性
到目前为止,在本章中我们看到的代码示例都是使用 JavaScript API 暴露的插件来调用插件功能。我们已经看到了 jQuery 对象方法、全局函数和自定义选择器是插件向脚本提供服务的一些方式。jQuery Mobile 库也有这些入口点,但与其进行交互的最常见方式是使用 HTML5 data 属性。
HTML5 规范允许我们在元素中插入任何我们想要的属性,只要属性以 data- 为前缀。在呈现页面时,这些属性将完全被忽略,但在我们的 jQuery 脚本中可以使用。当我们在页面中包含 jQuery Mobile 时,脚本会扫描页面寻找一些 data-* 属性,并将移动友好的特性添加到相应的元素。
jQuery Mobile 库会寻找几个特定的自定义 data 属性。我们将在第十二章中检查在我们自己的脚本中使用此功能的更一般的方法,高级 DOM 操作。
由于这种设计选择,我们将能够演示 jQuery Mobile 的一些强大特性,而无需自己编写任何 JavaScript 代码。
移动导航
jQuery Mobile 最显著的特性之一是它能够将页面上链接的行为简单地转变为 Ajax 驱动的导航。这种转变会为此过程添加简单的动画,同时保留了标准浏览器历史导航。为了看到这一点,我们将从一个呈现有关几本书信息的链接的文档开始(与我们之前用于构建幻灯片放映的相同内容),如下所示:
<!DOCTYPE html>
<html>
<head>
<title>jQuery Book Browser</title>
<link rel="stylesheet" href="booklist.css" type="text/css" />
<script src="img/jquery.js"></script>
</head>
<body>
<div>
<div>
<h1>Selected jQuery Books</h1>
</div>
<div>
<ul>
<li><a href="jq-game.html">jQuery Game Development
Essentials</a></li>
<li><a href="jqmobile-cookbook.html">jQuery Mobile
Cookbook</a></li>
<li><a href="jquery-designers.html">jQuery for
Designers</a></li>
<li><a href="jquery-hotshot.html">jQuery Hotshot</a></li>
<li><a href="jqui-cookbook.html">jQuery UI Cookbook</a></li>
<li><a href="mobile-apps.html">Creating Mobile Apps with
jQuery Mobile</a></li>
<li><a href="drupal-7.html">Drupal 7 Development by
Example</a></li>
<li><a href="wp-mobile-apps.html">WordPress Mobile
Applications with PhoneGap</a></li>
</ul>
</div>
</div>
</body>
</html>
在本章的可下载代码包中,可以在名为mobile.html的文件中找到完成的 HTML 示例页面。
到目前为止,我们还没有介绍 jQuery Mobile,页面呈现出默认的浏览器样式,正如我们所预期的那样。以下是屏幕截图:
我们的下一步是更改文档的<head>部分,以便引用 jQuery Mobile 及其样式表,如下所示:
<head>
<title>jQuery Book Browser</title>
<meta name="viewport"
content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="booklist.css"
type="text/css" />
<link rel="stylesheet"
href="jquery.mobile/jquery.mobile.css" type="text/css" />
<script src="img/jquery.js"></script>
<script src="img/jquery-migrate.js"></script>
<script src="img/jquery.mobile.js"></script>
</head>
请注意,我们还引入了一个定义页面视口的<meta>元素。这个声明告诉移动浏览器按照完全填充设备宽度的方式缩放文档的内容。
我们必须在页面中包含 jquery-migrate 插件,因为如果没有它,最新稳定版本的 jQuery 就不能与最新稳定版本的 jQuery Mobile 一起工作。想想这个问题。无论如何,一旦这两者正式配合起来,你可以简单地从页面中删除 jquery-migrate 插件。
jQuery Mobile 样式现在应用于我们的文档,显示出更大的无衬线字体,更新颜色和间距,如下图所示:
为了正确处理导航,jQuery Mobile 需要理解我们页面的结构。我们通过使用data-role属性来提供这些信息:
<div data-role="page">
<div data-role="header">
<h1>Selected jQuery Books</h1>
</div>
<div data-role="content">
<ul>
<li><a href="jq-game.html">jQuery Game Development
Essentials</a></li>
<li><a href="jqmobile-cookbook.html">jQuery Mobile
Cookbook</a></li>
<li><a href="jquery-designers.html">jQuery for
Designers</a></li>
<li><a href="jquery-hotshot.html">jQuery Hotshot</a></li>
<li><a href="jqui-cookbook.html">jQuery UI Cookbook</a></li>
<li><a href="mobile-apps.html">Creating Mobile Apps with
jQuery Mobile</a></li>
<li><a href="drupal-7.html">Drupal 7 Development by
Example</a></li>
<li><a href="wp-mobile-apps.html">WordPress Mobile
Applications with PhoneGap</a></li>
</ul>
</div>
</div>
现在页面加载时,jQuery Mobile 注意到我们有一个页面标题,并在页面顶部渲染出一个标准的移动设备标题栏:
当文本过长时超出标题栏,jQuery Mobile 会截断它,并在末尾添加省略号。在这种情况下,我们可以将移动设备旋转到横向方向以查看完整标题:
更重要的是,为了产生 Ajax 导航,这就是所需的全部内容。在从此列表链接到的页面上,我们使用类似的标记:
<div data-role="page">
<div data-role="header">
<h1>WordPress Mobile Applications with PhoneGap</h1>
</div>
<div data-role="content">
<img src="img/wp-mobile-apps.jpg" alt="WordPress Mobile
Applications with PhoneGap" />
<div class="title">WordPress Mobile Applications with
PhoneGap</div>
<div class="author">Yuxian Eugene Liang</div>
</div>
</div>
当点击到这个页面的链接时,jQuery Mobile 使用 Ajax 调用加载页面,抓取带有data-role="page"标记的文档部分,并使用淡入过渡显示这些内容:
在一个文档中提供多个页面
除了提供用于加载其他文档的 Ajax 功能外,jQuery Mobile 还提供了在单个文档中包含所有内容时提供相同用户体验的工具。为了实现这一点,我们只需使用标准的#符号将页面中的锚点链接起来,并将页面的那些部分标记为data-role="page",就像它们在单独的文档中一样,如下所示:
<div data-role="page">
<div data-role="header">
<h1>Selected jQuery Books</h1>
</div>
<div data-role="content">
<ul>
<li><a href="#jq-game">jQuery Game Development
Essentials</a></li>
<li><a href="#jqmobile-cookbook">jQuery Mobile
Cookbook</a></li>
<li><a href="#jquery-designers">jQuery for
Designers</a></li>
<li><a href="#jquery-hotshot">jQuery Hotshot</a></li>
<li><a href="#jqui-cookbook">jQuery UI Cookbook</a></li>
<li><a href="#mobile-apps">Creating Mobile Apps with jQuery
Mobile</a></li>
<li><a href="#drupal-7">Drupal 7 Development by
Example</a></li>
<li><a href="wp-mobile-apps.html">WordPress Mobile
Applications with PhoneGap</a></li>
</ul>
</div>
</div>
<div id="jq-game" data-role="page">
<div data-role="header">
<h1>jQuery Game Development Essentials</h1>
</div>
<div data-role="content">
<img src="img/jq-game.jpg" alt="jQuery Game Development
Essentials" />
<div class="title">jQuery Game Development Essentials</div>
<div class="author">Salim Arsever</div>
</div>
</div>
我们可以根据自己的方便选择这两种技术。将内容放在单独的文档中允许我们延迟加载信息,直到需要时,但这会增加一些开销,因为需要多个页面请求。
交互元素
jQuery Mobile 提供的功能主要是用于页面上的特定交互元素。这些元素增强了基本的网页功能,使页面组件在触摸界面上更加用户友好。其中包括手风琴式可折叠部分、切换开关、滑动面板和响应式表格。
jQuery UI 和 jQuery Mobile 提供的用户界面元素有很大的重叠。不建议在同一页上同时使用这两个库,但由于最重要的小部件都被两者提供,所以很少有这样的需要。
列表视图
由于它们的小型垂直屏幕布局,智能手机应用程序通常是以列表为主导的。我们可以使用 jQuery Mobile 轻松地增强页面上的列表,使它们的行为更像这些常见的本地应用程序元素。再次,我们只需引入 HTML5 自定义数据属性:
<ul data-role="listview" data-inset="true">
<li><a href="#jq-game">jQuery Game Development
Essentials</a></li>
<li><a href="#jqmobile-cookbook">jQuery Mobile Cookbook</a></li>
<li><a href="#jquery-designers">jQuery for Designers</a></li>
<li><a href="#jquery-hotshot">jQuery Hotshot</a></li>
<li><a href="#jqui-cookbook">jQuery UI Cookbook</a></li>
<li><a href="#mobile-apps">Creating Mobile Apps with jQuery
Mobile</a></li>
<li><a href="#drupal-7">Drupal 7 Development by Example</a></li>
<li><a href="wp-mobile-apps.html">WordPress Mobile Applications
with PhoneGap</a></li>
</ul>
添加 data-role="listview" 告诉 jQuery Mobile 将此列表中的链接设为大号,并且易于在触摸界面中用手指激活,而 data-inset="true" 则为列表提供了一个漂亮的边框,将其与周围内容分隔开来。结果是一个熟悉的、具有本地外观的控件,如下所示:
现在,我们有了大型触摸目标,但我们可以再进一步。移动应用程序中的类似列表视图通常会与搜索字段配对,以缩小列表中的项目。我们可以通过引入 data-filter 属性来添加这样一个字段,如下所示:
<ul data-role="listview" data-inset="true" data-filter="true">
结果是一个带有适当图标的圆角输入框,放置在列表上方:
尽管我们没有添加任何自己的代码,但这个搜索字段看起来不仅本地化,而且行为也是正确的:
工具栏按钮
另一个由 jQuery Mobile 增强的用户界面元素是简单按钮。就像 jQuery UI 允许我们标准化按钮外观一样,jQuery Mobile 增加了按钮的大小并修改了外观,以优化它们用于触摸输入。
在某些情况下,jQuery Mobile 甚至会为我们创建适当的按钮,在以前没有的情况下。例如,在移动应用程序的工具栏中通常有按钮。一个标准按钮是屏幕左上角的返回按钮,允许用户向上导航一级。如果我们为页面的 <div> 元素添加 data-add-back-btn 属性,我们就可以在不进行任何脚本工作的情况下获得此功能:
<div data-role="page" data-add-back-btn="true">
一旦添加了这个属性,每次导航到一个页面时,都会在工具栏上添加一个标准的返回按钮:
可以在 jquerymobile.com/ 找到用于初始化和配置 jQuery Mobile 小部件的完整 HTML5 数据属性列表。
高级功能
随着我们的移动页面需要更多定制设计元素和更复杂的交互,jQuery Mobile 提供了强大的工具来帮助我们创建它们。所有功能都在 jQuery Mobile 网站上有文档记录 (jquerymobile.com/)。虽然这些功能在此处详细讨论起来既过于高级又过于繁多,但还是值得简要提及一些:
-
移动友好事件:当在页面上引用 jQuery Mobile 时,我们的 jQuery 代码可以访问许多特殊事件,包括
tap、taphold和swipe。对于这些事件的处理程序可以与任何其他事件一样使用.on()方法进行绑定。特别是对于taphold和swipe,它们的默认配置(包括触摸持续时间)可以通过访问$.event.special.taphold和$.event.special.swipe对象的属性进行修改。除了基于触摸的事件外,jQuery Mobile 还提供了对滚动、方向更改以及其页面导航的各个阶段以及一组虚拟化鼠标事件的特殊事件的支持,这些事件对鼠标和触摸都做出反应。 -
主题化:与 jQuery UI 一样,jQuery Mobile 提供了一个 ThemeRoller。
(
jquerymobile.com/themeroller/) 用于自定义小部件的外观和感觉。 -
PhoneGap 集成:使用 jQuery Mobile 构建的站点可以轻松转换为原生移动应用程序,使用 PhoneGap(Cordova),可访问移动设备 API(如相机、加速计和地理位置)和应用商店。
$.support.cors和$.mobile.allowCrossDomainPages属性甚至可以允许访问不包含在应用程序中的页面,例如远程服务器上的页面。
总结
在本章中,我们探讨了如何将第三方插件整合到我们的网页中。我们仔细研究了 Cycle 插件、jQuery UI 和 jQuery Mobile,并在此过程中学习了我们将在其他插件中反复遇到的模式。在下一章中,我们将利用 jQuery 的插件架构开发一些不同类型的插件。
练习
-
将循环转换持续时间增加到半秒,并更改动画,使每个幻灯片在下一个幻灯片淡出之前淡入。参考循环文档以找到启用此功能的适当选项。
-
将
cyclePausedcookie 设置为持续 30 天。 -
限制标题框只能以十个像素为增量调整大小。
-
让滑块在幻灯片播放时从一个位置平稳地动画到下一个位置。
-
不要让幻灯片播放循环无限,使其在显示最后一张幻灯片后停止。当发生这种情况时,禁用按钮和滑块。
-
创建一个新的 jQuery UI 主题,具有浅蓝色小部件背景和深蓝色文本,并将主题应用到我们的示例文档中。
-
修改
mobile.html中的 HTML,使得列表视图按照书名的首字母分隔。详细信息请参阅 jQuery Mobile 文档中关于data-role="list-divider"的部分。