13.5 扩展 Ajax 功能
jQuery的Ajax框架不可谓不强大,这一点我们已经目睹了。但即便如此,我们仍然会遇到某些情况,希望能够改变这个框架的一些行为。没问题,jQuery为此提供了很多挂钩,可以让插件为它添加各种新功能。
13.5.1 数据类型转换器
第6章在介绍$.ajaxSetup()
函数时,我们知道通过它可以修改$.ajax()
使用的默认值,只用一条语句就可以影响后续的很多Ajax操作。通过这个函数,也可以为$.ajax()
添加它能够请求和解释的各种数据类型。
下面这个例子将创建一个能够解释YAML数据格式的转换器。YAML(http://www.yaml.org/)是一种流行的数据表示格式,很多语言都实现了对这种格式的支持。如果我们的脚本需要准备与这种格式交互,jQuery也可以让我们在原生的Ajax函数中添加对它的支持。
一个包含jQuery方法类别和子类别的YAML文件的示例如下:
Ajax:
- Global Ajax Event Handlers
- Helper Functions
- Low-Level Interface
- Shorthand Methods
Effects:
- Basics
- Custom
- Fading
- Sliding
我们可以给jQuery附加一个已有的YAML解析器,比如Diogo Costa开发的(http://code.google.com/p/javascript-yaml-parser/),从而让$.ajax()
能够解析这种格式。
要定义一种新的Ajax数据类型,需要给$.ajaxSetup()
传递三个参数:accepts
、contents
和converters
。其中,accepts
属性会添加发送到服务器的头部信息,声明我们的脚本可以理解的特定MIME类型;contents
属性处理数据交换的另一方,它提供一个与响应的MIME类型进行匹配的正则表达式,以尝试自动检测这个元数据当中的数据类型。最后,converters
中包含解析返回数据的函数,参见代码清单13-12。
代码清单13-12
$.ajaxSetup({
accepts: {
yaml: 'application/x-yaml, text/yaml'
},
contents: {
yaml: /yaml/
},
converters: {
'text yaml': function(textValue) {
console.log(textValue);
return '';
}
}
});
$.ajax({
url: 'categories.yml',
dataType: 'yaml'
});
在代码清单13-12中的这个特定的实现中,$.ajax()
读取了一个YAML文件并将数据库声明为yaml
。因为到来的数据会按照text
格式解析,jQuery需要一种机制能把一种数据类型转换为另一种数据类型。converters
的'text yaml'
告诉jQuery,这个转换函数以text
格式接收数据,然后以yaml
格式重新解析。
在转换函数内部,我们把文本内容记录到控制台中,以便验证这个函数能够被正确调用。要实际地执行转换,需要加载第三方的YAML解析库(yaml.js)并调用其方法,如代码清单13-13所示。
代码清单13-13
- $.ajaxSetup({
- accepts: {
- yaml: 'application/x-yaml, text/yaml'
- },
- contents: {
- yaml: /yaml/
- },
- converters: {
- 'text yaml': function(textValue) {
- var result = YAML.eval(textValue);
- var errors = YAML.getErrors();
- if (errors.length) {
- throw errors;
- }
- return result;
- }
- }
- });
- $.getScript('yaml.js').done(function() {
- $.ajax({
- url: 'categories.yml',
- dataType: 'yaml'
- }).done(function (data) {
- var cats = '';
- $.each(data, function(category, subcategories) {
- cats += '<li><a href="#">' + category + '</a></li>';
- });
- $(document).ready(function() {
- var $cats = $('#categories').removeClass('hide');
- $('<ul></ul>', {
- html: cats
- }).appendTo($cats);
- });
- });
- });
加载的yaml.js
文件中包含一个yaml
对象,该对象有.eval()
和.getErrors()
方法。我们就使用了这两个方法来解析收到的文本,然后以JavaScript对象的形式返回包含categories.yml中数据的结果。这个结果中的数据很容易通过遍历取得。因为这个文件中包含jQuery方法的类别,所以我们就使用解析后的结构来显示顶级类别,然后让用户通过单击这些顶级类别来筛选搜索结果,如图13-4所示。
图 13-4
这里要注意的是,在插入类别名的时候,需要把相应的代码放在$(doucment).ready()
调用中。Ajax操作可能会立即运行,无需访问DOM,但当结果返回后,必须等到DOM可用才能继续操作。以这种方式来编写代码,可以让它尽可能早地运行,从而增强用户对页面加载时间的感知速度。
接下来,我们处理单击类别链接的操作,如代码清单13-14所示。
代码清单13-14
$(document).on('click', '#categories a', function(event) {
event.preventDefault();
$(this).parent().toggleClass('active')
.siblings('.active').removeClass('active');
$('#ajax-form').triggerHandler('submit');
});
通过把click
处理程序绑定到文档并使用事件委托,可以避免某些耗时的重复性操作。而且,可以马上运行这些代码,而不必等待Ajax调用完成。
在这个处理程序中,要确保突出显示正确的类别,然后再触发表单的submit
处理程序。虽然突出显示如期生效,但表单还不能就被单击的类别名作出任何反应,如图13-5所示。
图 13-5
最后,就是更新表单的submit
处理程序,根据被激活的类别筛选方法,参见代码清单13-15。
代码清单13-15
- $ajaxForm.on('submit', function(event) {
- event.preventDefault();
- $response.empty();
- var title = $('#title').val(),
- category = $('#categories').find('li.active').text(),
- search = category + '-' + title;
- if (search == '-') {
- return;
- }
- $response.addClass('loading');
- if (!api[search]) {
- api[search] = $.ajax({
- url: 'http://book.learningjquery.com/api/',
- dataType: 'jsonp',
- data: {
- title: title,
- category: category
- },
- timeout: 15000
- });
- }
- api[search].done(response).fail(function() {
- $response.html(failed);
- }).always(function() {
- $response.removeClass('loading');
- });
- });
- $('#title').on('keyup', function(event) {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(function() {
- $ajaxForm.triggerHandler('submit');
- }, searchDelay);
- });
这里并没有简单地取得搜索字段的值,而是检索了激活的类别名的文本,然后通过Ajax同时传递这两个信息。而且,我们还修改了search
变量,让它既包含category也包含title。这样,搜索结果的缓存就能够正确地区分不同类别下相同文本的搜索。
现在,通过单击类别名称可以看到某个类别下的所有方法,也可以使用类别列表来筛选通过搜索字段搜索到的结果,如图13-6所示。
图 13-6
其他类似的数据类型也可以像这里定义YAML一样来定义。这样,就可以把jQuery的Ajax库改造成适合我们自己项目需要的得力工具。
13.5.2 Ajax预过滤器
通过$.ajaxPrefilter()
函数可以添加预过滤器。所谓预过滤器,就是一些回调函数,它们可以在发送请求之前对请求进行过滤。预过滤器会在$.ajax()
修改或使用它的任何选项之前调用,因此通过预过滤器可以修改这些选项或基于新的、自定义选项发送请求。
预过滤器通过返回要使用的数据类型,也可以操作请求的数据类型。前面YAML的例子中将数据类型指定为yaml
,是因为我们不想依赖服务器为响应提供正确的MIME类型。不过,我们倒是可以提供一个预过滤器,确保请求URL中包含相应的文件扩展名(.yml
),数据类型一定是yaml
,参见代码清单13-16。
代码清单13-16
$.ajaxPrefilter(function(options) {
if (/\.yml$/.test(options.url)) {
return 'yaml';
}
});
这里使用了一个简短的正则表达式测试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
$.ajaxTransport('img', function(settings) {
var $img, img, prop;
return {
send: function(headers, complete) {
function callback(success) {
if (success) {
complete(200, 'OK', {img: img});
} else {
$img.remove();
complete(404, 'Not Found');
}
}
$img = $('<img>', {
src: settings.url
});
img = $img[0];
prop = typeof img.naturalWidth === 'undefined' ? 'width' : 'naturalWidth';
if (img.complete) {
callback( !!img[prop] );
} else {
$img.on('load error', function(event) {
callback(event.type == 'load');
});
}
},
abort: function() {
if ($img) {
$img.remove();
}
}
};
});
在定义传输对象时,首先需要向$.ajaxTransport()
传入一个数据类型。这是告诉jQuery什么时候该使用我们的传输方式,而不是使用内置的机制。然后,再提供一个函数,该函数能够返回带有相应的.send()
和.abort()
方法的新传输对象。
对这个img
传输对象,.send()
方法需要创建一个新的<img>
元素,并为它设置src
特性。这个特性的值来自settings.url
,是由jQuery通过$.ajax()
调用传入的。浏览器在创建这个<img>
元素时,会加载引用的图像文件,因此在这里需要检查什么时候加载完成,然后触发完成回调函数。
如果想适应不同浏览器及不同版本,那就需要一些技巧才能正确地检测图像是否加载完成。在某些浏览器中,可以简单地给图像元素添加load
和error
事件处理程序。而在另一些浏览器中,当图像被缓存的时候,load
和error
不会像我们想象的那样被触发。
要了解浏览器加载图片时不一致的行为,读者可以参考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
$(document).ready(function() {
$.ajax({
url: 'missing.jpg',
dataType: 'img'
}).done(function(img) {
$('<div></div>', {
id: 'picture',
html: img
}).appendTo('body');
}).fail(function(xhr, textStatus, msg) {
$('<div></div>', {
id: 'picture',
html: textStatus + ': ' + msg
}).appendTo('body');
});
});
要使用自定义的传输机制,需要给$.ajax()
提供一个对应的dataType
值。然后,成功及失败处理程序要分别处理好各自接收到的数据。我们的img
传入机制在成功的时候返回一个<img>
DOM元素,所以.done()
处理程序就以这个元素作为新创建的<div>
元素的HTML内容,然后再将<div>
元素插入到文档中。
不过,在我的例子中,指定的文件(missing.jpg)并不存在;这就需要通过.fail()
处理程序来处理了。.fail()
处理程序会在<div>
元素中本应插入图像的地方插入一条错误消息,如图13-7所示。
图 13-7
只要引用一幅实际存在的图像,就可以避免出错,如代码清单13-19所示。
代码清单13-19
- $(document).ready(function() {
- $.ajax({
- url: 'sunset.jpg',
- dataType: 'img'
- }).done(function(img) {
- $('<div></div>', {
- id: 'picture',
- html: img
- }).appendTo('body');
- }).fail(function(xhr, textStatus, msg) {
- $('<div></div>', {
- id: 'picture',
- html: textStatus + ': ' + msg
- }).appendTo('body');
- });
- });
现在,我们定义的新的传输机制成功地加载了图像,可以在页面上看到图像了,如图13-8所示。
图 13-8
创建新的传输机制是一种不常见的需求,但即使是在这种情况下,jQuery的Ajax功能仍然能够满足我们的需要。