13.5 扩展 Ajax 功能

jQuery的Ajax框架不可谓不强大,这一点我们已经目睹了。但即便如此,我们仍然会遇到某些情况,希望能够改变这个框架的一些行为。没问题,jQuery为此提供了很多挂钩,可以让插件为它添加各种新功能。

13.5.1 数据类型转换器

第6章在介绍$.ajaxSetup()函数时,我们知道通过它可以修改$.ajax()使用的默认值,只用一条语句就可以影响后续的很多Ajax操作。通过这个函数,也可以为$.ajax()添加它能够请求和解释的各种数据类型。

下面这个例子将创建一个能够解释YAML数据格式的转换器。YAML(http://www.yaml.org/)是一种流行的数据表示格式,很多语言都实现了对这种格式的支持。如果我们的脚本需要准备与这种格式交互,jQuery也可以让我们在原生的Ajax函数中添加对它的支持。

一个包含jQuery方法类别和子类别的YAML文件的示例如下:

  1. Ajax:
  2. - Global Ajax Event Handlers
  3. - Helper Functions
  4. - Low-Level Interface
  5. - Shorthand Methods
  6. Effects:
  7. - Basics
  8. - Custom
  9. - Fading
  10. - Sliding

我们可以给jQuery附加一个已有的YAML解析器,比如Diogo Costa开发的(http://code.google.com/p/javascript-yaml-parser/),从而让$.ajax()能够解析这种格式。

要定义一种新的Ajax数据类型,需要给$.ajaxSetup()传递三个参数:acceptscontentsconverters。其中,accepts属性会添加发送到服务器的头部信息,声明我们的脚本可以理解的特定MIME类型;contents属性处理数据交换的另一方,它提供一个与响应的MIME类型进行匹配的正则表达式,以尝试自动检测这个元数据当中的数据类型。最后,converters中包含解析返回数据的函数,参见代码清单13-12。

代码清单13-12

  1. $.ajaxSetup({
  2. accepts: {
  3. yaml: 'application/x-yaml, text/yaml'
  4. },
  5. contents: {
  6. yaml: /yaml/
  7. },
  8. converters: {
  9. 'text yaml': function(textValue) {
  10. console.log(textValue);
  11. return '';
  12. }
  13. }
  14. });
  15. $.ajax({
  16. url: 'categories.yml',
  17. dataType: 'yaml'
  18. });

在代码清单13-12中的这个特定的实现中,$.ajax()读取了一个YAML文件并将数据库声明为yaml。因为到来的数据会按照text格式解析,jQuery需要一种机制能把一种数据类型转换为另一种数据类型。converters'text yaml'告诉jQuery,这个转换函数以text格式接收数据,然后以yaml格式重新解析。

在转换函数内部,我们把文本内容记录到控制台中,以便验证这个函数能够被正确调用。要实际地执行转换,需要加载第三方的YAML解析库(yaml.js)并调用其方法,如代码清单13-13所示。

代码清单13-13

  1. $.ajaxSetup({
  2. accepts: {
  3. yaml: 'application/x-yaml, text/yaml'
  4. },
  5. contents: {
  6. yaml: /yaml/
  7. },
  8. converters: {
  9. 'text yaml': function(textValue) {
  10. var result = YAML.eval(textValue);
  11. var errors = YAML.getErrors();
  12. if (errors.length) {
  13. throw errors;
  14. }
  15. return result;
  16. }
  17. }
  18. });
  19.  
  20. $.getScript('yaml.js').done(function() {
  21. $.ajax({
  22. url: 'categories.yml',
  23. dataType: 'yaml'
  24. }).done(function (data) {
  25. var cats = '';
  26. $.each(data, function(category, subcategories) {
  27. cats += '<li><a href="#">' + category + '</a></li>';
  28. });
  29.  
  30. $(document).ready(function() {
  31. var $cats = $('#categories').removeClass('hide');
  32. $('<ul></ul>', {
  33. html: cats
  34. }).appendTo($cats);
  35. });
  36. });
  37. });

加载的yaml.js文件中包含一个yaml对象,该对象有.eval().getErrors()方法。我们就使用了这两个方法来解析收到的文本,然后以JavaScript对象的形式返回包含categories.yml中数据的结果。这个结果中的数据很容易通过遍历取得。因为这个文件中包含jQuery方法的类别,所以我们就使用解析后的结构来显示顶级类别,然后让用户通过单击这些顶级类别来筛选搜索结果,如图13-4所示。

13.5 扩展 Ajax 功能 - 图1

图 13-4

这里要注意的是,在插入类别名的时候,需要把相应的代码放在$(doucment).ready()调用中。Ajax操作可能会立即运行,无需访问DOM,但当结果返回后,必须等到DOM可用才能继续操作。以这种方式来编写代码,可以让它尽可能早地运行,从而增强用户对页面加载时间的感知速度。

接下来,我们处理单击类别链接的操作,如代码清单13-14所示。

代码清单13-14

  1. $(document).on('click', '#categories a', function(event) {
  2. event.preventDefault();
  3. $(this).parent().toggleClass('active')
  4. .siblings('.active').removeClass('active');
  5. $('#ajax-form').triggerHandler('submit');
  6. });

通过把click处理程序绑定到文档并使用事件委托,可以避免某些耗时的重复性操作。而且,可以马上运行这些代码,而不必等待Ajax调用完成。

在这个处理程序中,要确保突出显示正确的类别,然后再触发表单的submit处理程序。虽然突出显示如期生效,但表单还不能就被单击的类别名作出任何反应,如图13-5所示。

13.5 扩展 Ajax 功能 - 图2

图 13-5

最后,就是更新表单的submit处理程序,根据被激活的类别筛选方法,参见代码清单13-15。

代码清单13-15

  1. $ajaxForm.on('submit', function(event) {
  2. event.preventDefault();
  3.  
  4. $response.empty();
  5.  
  6. var title = $('#title').val(),
  7. category = $('#categories').find('li.active').text(),
  8. search = category + '-' + title;
  9. if (search == '-') {
  10. return;
  11. }
  12.  
  13. $response.addClass('loading');
  14.  
  15. if (!api[search]) {
  16. api[search] = $.ajax({
  17. url: 'http://book.learningjquery.com/api/',
  18. dataType: 'jsonp',
  19. data: {
  20. title: title,
  21. category: category
  22. },
  23. timeout: 15000
  24. });
  25. }
  26. api[search].done(response).fail(function() {
  27. $response.html(failed);
  28. }).always(function() {
  29. $response.removeClass('loading');
  30. });
  31. });
  32.  
  33. $('#title').on('keyup', function(event) {
  34. clearTimeout(searchTimeout);
  35. searchTimeout = setTimeout(function() {
  36. $ajaxForm.triggerHandler('submit');
  37. }, searchDelay);
  38. });

这里并没有简单地取得搜索字段的值,而是检索了激活的类别名的文本,然后通过Ajax同时传递这两个信息。而且,我们还修改了search变量,让它既包含category也包含title。这样,搜索结果的缓存就能够正确地区分不同类别下相同文本的搜索。

现在,通过单击类别名称可以看到某个类别下的所有方法,也可以使用类别列表来筛选通过搜索字段搜索到的结果,如图13-6所示。

13.5 扩展 Ajax 功能 - 图3

图 13-6

其他类似的数据类型也可以像这里定义YAML一样来定义。这样,就可以把jQuery的Ajax库改造成适合我们自己项目需要的得力工具。

13.5.2 Ajax预过滤器

通过$.ajaxPrefilter()函数可以添加预过滤器。所谓预过滤器,就是一些回调函数,它们可以在发送请求之前对请求进行过滤。预过滤器会在$.ajax()修改或使用它的任何选项之前调用,因此通过预过滤器可以修改这些选项或基于新的、自定义选项发送请求。

预过滤器通过返回要使用的数据类型,也可以操作请求的数据类型。前面YAML的例子中将数据类型指定为yaml,是因为我们不想依赖服务器为响应提供正确的MIME类型。不过,我们倒是可以提供一个预过滤器,确保请求URL中包含相应的文件扩展名(.yml),数据类型一定是yaml,参见代码清单13-16。

代码清单13-16

  1. $.ajaxPrefilter(function(options) {
  2. if (/\.yml$/.test(options.url)) {
  3. return 'yaml';
  4. }
  5. });

这里使用了一个简短的正则表达式测试options.url中是否包含.yml。如果是,则将数据类型定义为yaml。有了这个预过滤器,就不需要给我们取得YAML文档的Ajax调用明确地定义数据类型了。

13.5.3 替代传输方式

我们已经看到jQuery在适当的时候会使用XMLHttpRequest、ActiveX或<script>标签来处理Ajax事务。如果我们愿意,也可扩展这种传输(transport)机制。

这种传输机制依赖于一个对象来实际地负责Ajax数据的传输。新的传输对象定义为工厂函数,返回一个带有.send().abort()方法的对象。其中,.send()方法负责发送请求、处理响应并把数据发送给回调函数。而.abort()方法会立即停止请求。

比如说,可以创建一个自定义的传输对象,使用<img>元素来取得外部数据。也就是说,加载图像的处理方式会与其他Ajax请求的处理方式相同,这样会让代码在内部更好地保持一致。创建这个新传输对象的JavaScript代码并不那么简单,因此我们直接看完成后的结果,然后再分析关键代码的作用,参见代码清单13-17。

代码清单13-17

  1. $.ajaxTransport('img', function(settings) {
  2. var $img, img, prop;
  3. return {
  4. send: function(headers, complete) {
  5. function callback(success) {
  6. if (success) {
  7. complete(200, 'OK', {img: img});
  8. } else {
  9. $img.remove();
  10. complete(404, 'Not Found');
  11. }
  12. }
  13. $img = $('<img>', {
  14. src: settings.url
  15. });
  16. img = $img[0];
  17. prop = typeof img.naturalWidth === 'undefined' ? 'width' : 'naturalWidth';
  18. if (img.complete) {
  19. callback( !!img[prop] );
  20. } else {
  21. $img.on('load error', function(event) {
  22. callback(event.type == 'load');
  23. });
  24. }
  25. },
  26. abort: function() {
  27. if ($img) {
  28. $img.remove();
  29. }
  30. }
  31. };
  32. });

在定义传输对象时,首先需要向$.ajaxTransport()传入一个数据类型。这是告诉jQuery什么时候该使用我们的传输方式,而不是使用内置的机制。然后,再提供一个函数,该函数能够返回带有相应的.send().abort()方法的新传输对象。

对这个img传输对象,.send()方法需要创建一个新的<img>元素,并为它设置src特性。这个特性的值来自settings.url,是由jQuery通过$.ajax()调用传入的。浏览器在创建这个<img>元素时,会加载引用的图像文件,因此在这里需要检查什么时候加载完成,然后触发完成回调函数。

如果想适应不同浏览器及不同版本,那就需要一些技巧才能正确地检测图像是否加载完成。在某些浏览器中,可以简单地给图像元素添加loaderror事件处理程序。而在另一些浏览器中,当图像被缓存的时候,loaderror不会像我们想象的那样被触发。

 要了解浏览器加载图片时不一致的行为,读者可以参考Lucas Smith的博客文章“我的图片加载了吗?”(Is My Image Loaded?),地址是:http://www.verious.com/tool/is-my-image-loaded/

代码清单13-17中的代码可视情况通过检测.complete.width.naturalWidth属性来解决这个问题。在检测到图像加载完成后(可能成功、完成,也可能出错),调用callback()函数,callback()函数再调用传递给.send()complete()函数。这样,$.ajax()就能对图像的加载给出响应。

对于停止加载的处理就要简单多了。这里的.abort()方法要做的就是一些清理工作,它只需要在创建了<img>元素的情况下把该元素删除即可。

接下来,我们就写一个$.ajax()调用来使用这个新的传输机制,参见代码清单13-18。

代码清单13-18

  1. $(document).ready(function() {
  2. $.ajax({
  3. url: 'missing.jpg',
  4. dataType: 'img'
  5. }).done(function(img) {
  6. $('<div></div>', {
  7. id: 'picture',
  8. html: img
  9. }).appendTo('body');
  10. }).fail(function(xhr, textStatus, msg) {
  11. $('<div></div>', {
  12. id: 'picture',
  13. html: textStatus + ': ' + msg
  14. }).appendTo('body');
  15. });
  16. });

要使用自定义的传输机制,需要给$.ajax()提供一个对应的dataType值。然后,成功及失败处理程序要分别处理好各自接收到的数据。我们的img传入机制在成功的时候返回一个<img>DOM元素,所以.done()处理程序就以这个元素作为新创建的<div>元素的HTML内容,然后再将<div>元素插入到文档中。

不过,在我的例子中,指定的文件(missing.jpg)并不存在;这就需要通过.fail()处理程序来处理了。.fail()处理程序会在<div>元素中本应插入图像的地方插入一条错误消息,如图13-7所示。

13.5 扩展 Ajax 功能 - 图4

图 13-7

只要引用一幅实际存在的图像,就可以避免出错,如代码清单13-19所示。

代码清单13-19

  1. $(document).ready(function() {
  2. $.ajax({
  3. url: 'sunset.jpg',
  4. dataType: 'img'
  5. }).done(function(img) {
  6. $('<div></div>', {
  7. id: 'picture',
  8. html: img
  9. }).appendTo('body');
  10. }).fail(function(xhr, textStatus, msg) {
  11. $('<div></div>', {
  12. id: 'picture',
  13. html: textStatus + ': ' + msg
  14. }).appendTo('body');
  15. });
  16. });

现在,我们定义的新的传输机制成功地加载了图像,可以在页面上看到图像了,如图13-8所示。

13.5 扩展 Ajax 功能 - 图5

图 13-8

创建新的传输机制是一种不常见的需求,但即使是在这种情况下,jQuery的Ajax功能仍然能够满足我们的需要。