6.1 基于请求加载数据

在所有炒作和粉饰的背后,Ajax只不过是一种无需刷新页面即可从服务器(或客户端)上加载数据的手段。这些数据的格式可以是很多种,而且,当数据到达时也有很多处理它们的方法可供选择。本章后面,当我们以多种方式完成同样的基本任务时,就能够清楚地看到这一点。

假设我们要创建一个页面,用以显示字典中的词条,词条按照英文首字母分组。那么,定义页面内容区的HTML代码可以像这下面这样:

  1. <div id="dictionary">
  2. </div>

对,没错!这个页面一开始没有内容。下面我们将使用jQuery的各种Ajax方法取得字典词条并用来填充这个<div>

 下载代码示例

如同本书其他HTML、CSS以及JavaScript示例一样,上面的标记只是完整文档的一个片段。如果读者想试一试这些示例,可以从以下地址下载完整的示例代码: Packt Publishing 网站 http://www.packtpub.com/support ,或者本书网站 http://book.learningjquery.com/

因为需要一种触发加载过程的方式,所以我们添加了几个调用事件处理程序的按钮:

  1. <div class="letters">
  2. <div class="letter" id="letter-a">
  3. <h3><a href="entries-a.html">A</a></h3>
  4. </div>
  5. <div class="letter" id="letter-b">
  6. <h3><a href="entries-a.html">B</a></h3>
  7. </div>
  8. <div class="letter" id="letter-c">
  9. <h3><a href="entries-a.html">C</a></h3>
  10. </div>
  11. <div class="letter" id="letter-d">
  12. <h3><a href="entries-a.html">D</a></h3>
  13. </div>
  14. <!-- and so on -->
  15. </div>

这些简单的链接可以把我们引导到包含字母对应的字典条目的页面。我们将根据渐进增强的原则,让这些链接在不重新加载整个页面的前提下更新页面。

再添加一些CSS规则,就得到了如图6-1所示的页面。

6.1 基于请求加载数据 - 图1

图 6-1

下面,我们关注的焦点就是如何向页面中填充内容。

6.1 追加HTML

Ajax应用程序通常只不过是一个针对HTML代码块的请求。这种被称作AHAH(Asynchronous HTTP and HTML,异步HTTP和HTML)的技术,通过jQuery来实现只是小菜一碟。首先,需要一些供插入用的HTML,我们把这些HTML放在与主文档位于同一目录下的a.html文件中。第二个HTML文件开始处的代码如下:

  1. <div class="entry">
  2. <h3 class="term">ABDICATION</h3>
  3. <div class="part">n.</div>
  4. <div class="definition">
  5. An act whereby a sovereign attests his sense of the high
  6. temperature of the throne.
  7. <div class="quote">
  8. <div class="quote-line">Poor Isabella's Dead, whose
  9. abdication</div>
  10. <div class="quote-line">Set all tongues wagging in the
  11. Spanish nation.</div>
  12. <div class="quote-line">For that performance 'twere
  13. unfair to scold her:</div>
  14. <div class="quote-line">She wisely left a throne too
  15. hot to hold her.</div>
  16. <div class="quote-line">To History she'll be no royal
  17. riddle &mdash; </div>
  18. <div class="quote-line">Merely a plain parched pea that
  19. jumped the griddle.</div>
  20. <div class="quote-author">G.J.</div>
  21. </div>
  22. </div>
  23. </div>
  24. <div class="entry">
  25. <h3 class="term">ABSOLUTE</h3>
  26. <div class="part">adj.</div>
  27. <div class="definition">
  28. Independent, irresponsible. An absolute monarchy is one
  29. in which the sovereign does as he pleases so long as he
  30. pleases the assassins. Not many absolute monarchies are
  31. left, most of them having been replaced by limited
  32. monarchies, where the sovereign's power for evil (and for
  33. good) is greatly curtailed, and by republics, which are
  34. governed by chance.
  35. </div>
  36. </div>

这个页面的HTML代码中还包含更多词条。单独查看这个文档,结果显示它非常简单,如图6-2所示。

6.1 基于请求加载数据 - 图2

图 6-2

我们注意到,a.html并不是一个真正的HTML文档,它不包含<html><head>或者<body>,只包含最基本的代码。通常,我们把这种文件叫做片段;它唯一目的就是供插入到其他HTML文档中使用,插入的过程如代码清单6-1所示。

代码清单6-1

  1. $(document).ready(function() {
  2. $('#letter-a a').click(function(event) {
  3. event.preventDefault();
  4. $('#dictionary').load('a.html');
  5. });
  6. });

其中,.load()方法替我们完成了所有烦琐复杂的工作!这里,我们通过常规的jQuery选择符为HTML片段指定了目标位置,然后将要加载的文件的URL作为参数传递给.load()方法。现在,当单击第1个按钮时,这个文件就会被加载并插入到<div id="dictionary">内部。而且,当插入完成后,浏览器会立即呈现新的HTML,如图6-3所示。

6.1 基于请求加载数据 - 图3

图 6-3

从图6-3中可以看出,虽然这个HTML片段之前没有样式,但现在已经应用了样式。这些样式是主文档中的CSS规则所添加的,即当新HTML片段插入时,相应的CSS规则也会立即应用到它的标签上。

测试这个例子:当单击按钮时,字典中的解释会立即出现。这只是在本地运行应用程序的一种特殊情况。如果通过网络来传递相同的文档,那么需要多长的时间延迟或中断是很难估计的。下面我们添加一个警告框,使其在加载完解释内容后立即显示,参见代码清单6-2。

代码清单6-2

  1. $(document).ready(function() {
  2. $('#letter-a a').click(function(event) {
  3. event.preventDefault();
  4. $('#dictionary').load('a.html');
  5. alert('Loaded!');
  6. });
  7. });

根据代码中的结构,你可能会认为警告框只有在加载过程完成后才会显示。因为JavaScript通常以同步方式执行代码,即严格按照顺序逐行执行。

然而,当我们在运行中的服务器上测试上面这些代码时,由于网络延迟,警告框很可能先于加载完成就出现了。这是因为所有Ajax请求在默认情况下都是异步的,否则,我们就要称它为Sjax了1,而后者显然难以与Ajax相提并论2

1 S是synchronous的首字母,即同步。

2 作者这里的意思是,如果不是Ajax,而是SJAX,即不是异步加载,而是同步加载,那么就不会有那么大的影响了。

异步加载意味着在发出取得HTML片段的HTTP请求后,会立即恢复脚本执行,无需等待。在之后的某个时刻,当浏览器收到服务器的响应时,再对响应的数据进行处理。这通常都是人们期望的行为,但它不会导致在等待数据返回期间锁定整个Web浏览器。

对于必须要延迟到加载完成才能继续的操作,jQuery提供了一个回调函数。我们在第4章就已经使用过回调函数了。通过回调函数可以在某些效果完成之后执行操作。Ajax回调的功能与此类似,只不过是在数据从服务器返回后执行操作。本章后面将会在学习从服务器加载JSON数据时展示使用回调函数的例子。

6.1.2 操作JavaScript对象

通过请求获取充分格式化的HTML虽然很方便,但这也意味着必须在传输文本内容的同时也传输很多HTML标签。有时候,我们希望能够尽量少传输一些数据,然后马上处理这些数据。在这种情况,我们希望取得能够通过JavaScript进行遍历的数据结构。

使用jQuery的选择符可以遍历和操作取得的HTML结构,但是还有一种JavaScript内置的数据格式,既能减少数据传输量,也会减少编码量。

  • 取得JSON

前面我们曾经看到过,JavaScript对象是由一些“键—值”对组成的,而且还可以方便地使用花括号({})来定义。另一方面,JavaScript的数组则可以使用方括号([])和隐式声明的逐渐递增的键进行动态定义。将这两种语法组合起来,可以轻松地表达复杂而且庞大的数据结构。

Douglas Crockford为这种简单的语法起了一个名字,叫做JSON(JavaScript Object Notation,JavaScript对象表示法)。通过这种表示法能够方便地取代数据量庞大的XML格式:

  1. {
  2. "key": "value",
  3. "key 2": [
  4. "array",
  5. "of",
  6. "items"
  7. ]
  8. }

对象字面量数组字面量的基础上,JSON格式的语法具有很强的表达能力,但对其中的值也有一定的限制。例如,JSON规定所有对象键以及所有字符串值,都必须包含在双引号中。而且,函数也不是有效的JSON值。由于存在这些限制,开发人员最好不手工编辑JSON,而应该用服务器端语言来生成。

 要了解JSON的语法要求以及它有哪些优势,都有哪些语言支持这种数据格式,请访问http://json.org/

如果用这种格式对字典中的解释进行编码,那么可能会有很多种编码方式。这里,我们把一些字典的词条放在一个名叫b.json的JSON文件中,这个文件开头部分的代码如下:

  1. [
  2. {
  3. "term": "BACCHUS",
  4. "part": "n.",
  5. "definition": "A convenient deity invented by the...",
  6. "quote": [
  7. "Is public worship, then, a sin,",
  8. "That for devotions paid to Bacchus",
  9. "The lictors dare to run us in,",
  10. "And resolutely thump and whack us?"
  11. ],
  12. "author": "Jorace"
  13. },
  14. {
  15. "term": "BACKBITE",
  16. "part": "v.t.",
  17. "definition": "To speak of a man as you find him when..."
  18. },
  19. {
  20. "term": "BEARD",
  21. "part": "n.",
  22. "definition": "The hair that is commonly cut off by..."
  23. },
  24. ... file continues ...

要取得这些数据,可以使用$.getJSON()方法,这个方法会在取得相应文件后对文件进行处理。在数据从服务器返回后,它只是一个简单的JSON格式的文本字符串。$.getJSON()方法会解析这个字符串,并将处理得到的JavaScript对象提供给调用代码。

  • 使用全局jQuery函数

到目前为止,我们使用的所有jQuery方法都需要通过$()函数构建的一个jQuery对象进行调用。通过选择符表达式,我们可以指定一组要操作的DOM节点,然后再用这些jQuery方法以某种方式对它们进行操作。然而,$.getJSON()函数却不一样。从逻辑上说,没有该方法适用的DOM元素;作为结果的对象只能提供给脚本,而不能插入到页面中。为此,getJSON()是作为全局jQuery对象(由jQuery库定义的jQuery$对象)的方法定义的,也就是说,它不是个别jQuery对象实例(即通过$()函数创建的对象)的方法。

如果JavaScript中有类似其他面向对象语言中的类,那我们可以把$.getJSON()称为类方法。为了便于理解,我们在这里称其为全局函数;实际上,为了不与其他函数名称发生冲突,这些全局函数使用的是jQuery命名空间

在使用这个函数时,我们还需要像以前一样为它传递文件名,如代码清单6-3所示。

代码清单6-3

  1. //未完成的代码
  2. $(document).ready(function() {
  3. $('#letter-b a').click(function(event) {
  4. event.preventDefault();
  5. $.getJSON('b.json');
  6. });
  7. });

当单击按钮时,我们看不到以上代码的效果。因为虽然函数调用加载了文件,但是并没有告诉JavaScript对返回的数据如何处理。为此,我们需要使用一个回调函数。

$.getJSON()函数可以接受第2个参数,这个参数是当加载完成时调用的函数。如上所述,Ajax请求都是异步的,回调函数提供了一种等待数据返回的方式,而不是立即执行代码。回调函数也需要一个参数,该参数中保存着返回的数据。因此,我们的代码要写成代码清单6-4这样。

代码清单6-4

  1. //未完成的代码
  2. $(document).ready(function() {
  3. $('#letter-b a').click(function(event) {
  4. event.preventDefault();
  5. $.getJSON('b.json', function(data) {
  6. });
  7. });
  8. });

我们在此使用了匿名函数表达式作为回调函数,这在jQuery代码中很常见,主要是为了保持代码简洁。当然,对函数声明的引用同样也可以作为回调函数。

这样,我们就可以在函数中通过data变量来遍历JSON数据结构了。具体来说,需要迭代顶级数组,为每个项构建相应的HTML代码。虽然可以在这里使用标准的for循环,但我们要借此机会介绍jQuery的另一个实用全局函数$.each(),在第5章中,我们曾看到过它的对应方法.each()$.each()函数不操作jQuery对象,它以数组或对象作为第一个参数,以回调函数作为第二个参数。此外,还需要将每次循环中数组或对象的当前索引和当前项作为回调函数的两个参数,参见代码清单6-5。

代码清单6-5

  1. $(document).ready(function() {
  2. $('#letter-b a').click(function(event) {
  3. event.preventDefault();
  4. $.getJSON('b.json', function(data) {
  5. var html = '';
  6. $.each(data, function(entryIndex, entry) {
  7. html += '<div class="entry">';
  8. html += '<h3 class="term">' + entry.term + '</h3>';
  9. html += '<div class="part">' + entry.part + '</div>';
  10. html += '<div class="definition">';
  11. html += entry.definition;
  12. html += '</div>';
  13. html += '</div>';
  14. });
  15. $('#dictionary').html(html);
  16. });
  17. });
  18. });

这里通过$.each()函数依次遍历每个项,并使用entry对象的内容构建起HTML代码结构。构建好HTML之后,通过.html()把它插入到<div id="dictionary">中,替换其中原有的所有内容。

 安全的HTML

这种方法要求数据中包含可以直接用来构建HTML的安全内容,例如,数据中不能包含任何<字符。

现在所剩的就是处理词条中的引用语了,这需要使用另一个$.each()循环,参见代码清单6-6。

代码清单6-6

  1. $(document).ready(function() {
  2. $('#letter-b a').click(function(event) {
  3. event.preventDefault();
  4. $.getJSON('b.json', function(data) {
  5. var html = '';
  6. $.each(data, function(entryIndex, entry) {
  7. html += '<div class="entry">';
  8. html += '<h3 class="term">' + entry.term + '</h3>';
  9. html += '<div class="part">' + entry.part + '</div>';
  10. html += '<div class="definition">';
  11. html += entry.definition;
  12. if (entry.quote) {
  13. html += '<div class="quote">';
  14. $.each(entry.quote, function(lineIndex, line) {
  15. html += '<div class="quote-line">' + line + '</div>';
  16. });
  17. if (entry.author) {
  18. html += '<div class="quote-author">' + entry.author + '</div>';
  19. }
  20. html += '</div>';
  21. }
  22. html += '</div>';
  23. html += '</div>';
  24. });
  25. $('#dictionary').html(html);
  26. });
  27. });
  28. });

编写完这些代码后,就可以单击下一个B链接来验证我们的成果了,如图6-4所示,页面右侧出现了相应的字典条目。

6.1 基于请求加载数据 - 图4

图 6-4

尽管JSON格式很简洁,但它却不容许任何错误。所有方括号、花括号、引号和逗号都必须合理且正确地使用,否则文件不会加载。而且,在多数浏览器中,当文件加载失败时我们看不到任何错误信息;脚本只是静默地彻底终止运转。

  • 执行脚本

有时候,在页面初次加载时就取得所需的全部JavaScript也是没有必要的。具体需要取得哪个脚本,要视用户的操作而定。虽然可以在需要时动态地引入<script>标签,但注入所需代码的更优雅的方式则是通过jQuery直接加载.js文件。

向页面中注入脚本与加载HTML片段一样简单。但在这种情况下,需要使用全局函数$.getScript(),这个全局函数与它的同辈函数类似,接受一个URL参数以查找脚本文件,参见代码清单6-7。

代码清单6-7

  1. $(document).ready(function() {
  2. $('#letter-c a').click(function(event) {
  3. event.preventDefault();
  4. $.getScript('c.js');
  5. });
  6. });

在前一个例子中,接下来要做的应该是处理结果数据,以便有效地利用加载的文件。然而,对于一个脚本文件来说,这个过程是自动化;换句话说,脚本会自动执行。

以这种方式取得的脚本会在当前页面的全局环境下执行。这意味着脚本有权访问在全局环境中定义的函数和变量,当然也包括jQuery自身。因而,我们可以模仿JSON的例子来准备脚本代码,以便在脚本执行时将HTML插入到页面中。现在,将以下脚本代码保存到c.js中:

  1. var entries = [
  2. {
  3. "term": "CALAMITY",
  4. "part": "n.",
  5. "definition": "A more than commonly plain and..."
  6. },
  7. {
  8. "term": "CANNIBAL",
  9. "part": "n.",
  10. "definition": "A gastronome of the old school who..."
  11. },
  12. {
  13. "term": "CHILDHOOD",
  14. "part": "n.",
  15. "definition": "The period of human life intermediate..."
  16. }
  17. //省略的内容
  18. ];
  19.  
  20. var html = '';
  21.  
  22. $.each(entries, function() {
  23. html += '<div class="entry">';
  24. html += '<h3 class="term">' + this.term + '</h3>';
  25. html += '<div class="part">' + this.part + '</div>';
  26. html += '<div class="definition">' + this.definition + '</div>';
  27. html += '</div>';
  28. });
  29.  
  30. $('#dictionary').html(html);

最后,单击C链接,应该会看到我们预期的结果。

6.1.3 加载XML文档

XML是缩写词Ajax中的一部分,但我们至今还没有谈到加载XML文档。加载XML文档很简单,而且与JSON技术也相当接近。首先,需要将希望显示的数据包含在一个名为d.xml的XML文件中:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <entries>
  3. <entry term="DEFAME" part="v.t.">
  4. <definition>
  5. To lie about another. To tell the truth about another.
  6. </definition>
  7. </entry>
  8. <entry term="DEFENCELESS" part="adj.">
  9. <definition>
  10. Unable to attack.
  11. </definition>
  12. </entry>
  13. <entry term="DELUSION" part="n.">
  14. <definition>
  15. The father of a most respectable family, comprising
  16. Enthusiasm, Affection, Self-denial, Faith, Hope,
  17. Charity and many other goodly sons and daughters.
  18. </definition>
  19. <quote author="Mumfrey Mappel">
  20. <line>All hail, Delusion! Were it not for thee</line>
  21. <line>The world turned topsy-turvy we should see;
  22. </line>
  23. <line>For Vice, respectable with cleanly fancies,
  24. </line>
  25. <line>Would fly abandoned Virtue's gross advances.
  26. </line>
  27. </quote>
  28. </entry>
  29. </entries>

当然,通过XML来表示这些数据的形式有很多种,而其中一些能够非常近似地模仿我们已经确定的HTML结构或者前面使用的JSON。不过,这里我们示范了XML的一些更方便阅读的特性,例如使用属性termpart,而不是标签

下面以我们熟悉的方式开始编写函数,参见代码清单6-8。

代码清单6-8

  1. //未完成的代码
  2. $(document).ready(function() {
  3. $('#letter-d a').click(function(event) {
  4. event.preventDefault();
  5. $.get('d.xml', function(data) {
  6. });
  7. });
  8. });

这次,帮助我们完成任务的是$.get()函数。通常,这个函数只是取得由URL指定的文件,然后将纯文本格式的数据提供给回调函数。但是,在根据服务器提供的MIME类型知道响应的是XML的情况下,提供给回调函数的将是XML DOM树。

好在,我们已经领略过了jQuery强大的DOM遍历能力。遍历XML文档的方式同HTML文档一样,也可以使用常规的.find().filter()及其他遍历方法,参见代码清单6-9。

代码清单6-9

  1. $(document).ready(function() {
  2. $('#letter-d a').click(function(event) {
  3. event.preventDefault();
  4. $.get('d.xml', function(data) {
  5. $('#dictionary').empty();
  6. $(data).find('entry').each(function() {
  7. var $entry = $(this);
  8. var html = '<div class="entry">';
  9. html += '<h3 class="term">' + $entry.attr('term');
  10. html += '</h3>';
  11. html += '<div class="part">' + $entry.attr('part');
  12. html += '</div>';
  13. html += '<div class="definition">';
  14. html += $entry.find('definition').text();
  15. var $quote = $entry.find('quote');
  16. if ($quote.length) {
  17. html += '<div class="quote">';
  18. $quote.find('line').each(function() {
  19. html += '<div class="quote-line">';
  20. html += $(this).text() + '</div>';
  21. });
  22. if ($quote.attr('author')) {
  23. html += '<div class="quote-author">';
  24. html += $quote.attr('author') + '</div>';
  25. }
  26. html += '</div>';
  27. }
  28. html += '</div>';
  29. html += '</div>';
  30. $('#dictionary').append($(html));
  31. });
  32. });
  33. });
  34. });

这样,当单击D链接时,也可以得到预期的效果,如图6-5所示。

6.1 基于请求加载数据 - 图5

图 6-5

这是我们已知的DOM遍历方法的新用途,而且,jQuery对CSS选择符支持的灵活性由此也可见一斑。CSS的选择符语法一般适合美化HTML页面,位于标准.css文件中的选择符,如divbody等标签名都是为了在HTML中找到内容。然而,jQuery可以使用任意XML标签名,如这里的entrydifinition,就和使用标准HTML标签一样方便。

jQuery内部先进的选择符引擎,对于在更复杂的情况下查找XML文档中的元素同样很有帮助。例如,假设我们想把显示的内容限定为那些带有引用进而带有作者属性的词条。那么,通过将entry修改为entry:has(quote)就可以把词条限定为必须包含嵌套的引用元素。然后,还可以通过entry:has(quote[author])进一步限定词条中的引用元素必须包含author属性。

这样,代码清单6-9中带有初始选择符的代码行应该写成:

  1. $(data).find('entry:has(quote[author])').each(function() {

由图6-6可以看出,新的选择符表达式对返回的词条进行了适当的限制。

6.1 基于请求加载数据 - 图6

图 6-6