本文转自:
http://www.infoq.com/cn/articles/dojo-ajax-xhr
在任何浏览器上方便地实现Ajax请求是每一个Ajax框架的初衷。Dojo在这方面无疑提供了非常丰富的支持。除了XMLHttpRequest之外,动态script、iframe、RPC也应有尽有,并且接口统一,使用方便,大多数情况下都只需要一句话就能达到目的,从而免除重复造轮子的麻烦。而且,Dojo一贯追求的概念完整性也在这里有所体现,换句话说,在使用Dojo的Ajax工具的过程中不会感到任何的不自然,相反更容易有触类旁通的感觉,因为API的模式是统一的,而且这里涉及到的某些概念(如Deferred对象)也贯穿在整个Dojo之中。
Dojo的XHR函数
Dojo的XMLHttpRequest函数就叫dojo.xhr,除了把自己取名美元符号之外,这好像是最直接的办法了。它定义在Dojo基本库里,所以不需要额外的require就能使用。它可以实现任何同域内的http请求。不过更常用的是dojo.xhrGet和dojo.xhrPost,它们只不过是对dojo.xhr函数的简单封装;当然根据REST风格,还有dojo.xhrPut和dojo.xhrDelete。
这些函数的参数都很统一。除了dojo.xhr的第一个参数是http方法名之外,所有的dojo.xhr*系列函数都接受同一种散列式的参数,其中包含请求的细节,例如url、是否同步、要传给服务器的内容(可以是普通对象、表单、或者纯文本)、超时设定、返回结果的类型(非常丰富且可扩展)、以及请求成功和失败时的回调。所有dojo.xhr*函数(实际上是所有IO函数)返回值也都一样,都是一个Deferred对象,顾名思义,它能让一些事情“延迟”发生,从而让API用起来更灵活。
下面的两个例子可能会带来一点直观感受:
dojo.xhrGet({ url: "something.html", load: function(response, ioArgs){ //用response干一些事 console.log("xhr get success:", response); return response; //必须返回response }, error: function(response, ioArgs){ console.log("xhr get failed:", response); return response; //必须返回response } }); //Deferred对象允许用同步调用的写法写异步调用 var deferredResult = dojo.xhrPost({ url: "something.html", form: formNode, //Dojo会自动将form转成object timeout: 3000, //Dojo会保证超时设定的有效性 handleAs: "json" //得到的response将被认为是JSON,并自动转为object }); //当响应结果可用时再调用回调函数 deferredResult.then(function(response){ console.log("xhr get success:", response); return response; //必须返回response });
首先解释一下timeout。除了IE8之外,目前大多数XMLHttpRequest对象都没有内置的timeout功能,因此必须用 setTimeout。当同时存在大量请求时,需要为每一个请求设置单独的定时器,这在某些浏览器(主要是IE)会造成严重的性能问题。dojo的做法是只用一个单独的setInterval,定时轮询(间隔50ms)所有还未结束的请求的状态,这样就高效地解决了一切远程请求(包括JSONP和 iframe)的超时问题。
值得一提的还有handleAs参数,通过设置这个参数,可以自动识别服务器的响应内容格式并转换成对象或文本等方便使用的形式。根据文档,它接受如下值:text (默认), json, json-comment-optional, json-comment-filtered, javascript, xml。
而且它还是可扩展的。其实handleAs只是告诉xhr函数去调用哪个格式转换插件,即dojo.contentHandlers对象里的一个方法。例如 dojo.contentHandlers.json就是处理JSON格式的插件。你可以方便地定制自己所需要的格式转换插件,当然,你也可修改现有插件的行为:
dojo.contentHandlers.json = (function(old){ return function(xhr){ var json = old(xhr); if(json.someSignalFormServer){ doSomthing(json); delete json.someSignalFormServer; } return json; } })(dojo.contentHandlers.json);//一个小技巧,利用传参得到原方法
如果要了解每个参数的细节,可以参考Dojo的文档。
虚拟的参数类
这里特别提一下Dojo在API设计上的两个特点。其一是虚拟的参数“类”概念:通过利用javascript对象可以灵活扩展的特点,强行规定一个散列参数属于某个“类”。例如dojo.xhr*系列函数所接受的参数就称为dojo.__XhrArgs。这个“类”并不存在于实际代码中(不要试图用 instanceof验证它),只停留在概念上,比抽象类还抽象,因此给它加上双下划线前缀(Dojo习惯为抽象类加单下划线前缀)。这样做看起来没什么意思,但实际上简化了API,因为它使API之间产生了联系,更容易记忆也就更易于使用。这一点在对这种类做“继承”时更明显。例如 dojo.__XhrArgs继承自dojo.__IoArgs,这是所有IO函数所必须支持的参数集合,同样继承自dojo.__IoArgs的还有 dojo.io.script.__ioArgs和dojo.io.iframe.__ioArgs,分别用于动态脚本请求和iframe请求。子类只向父类添加少量的属性,这样繁多的参数就具有了树形类结构。原本散列式参数是用精确的参数名代替了固定的参数顺序,在增加灵活性和可扩展性的同时,实际上增加了记忆量(毕竟参数名不能拼错),使得API都不像看起来那么好用,有了参数类的设计就缓解了这个问题。
这种参数类的做法在Dojo里随处可见,读源码的话就会发现它们都是被正儿八经地以正常代码形式声明在一种特殊注释格式里的,像这样:
/*===== dojo.declare("dojo.__XhrArgs", dojo.__IoArgs, { constructor: function(){ //summary: //... //handleAs: //... //...... } }); =====*/
这种格式可以被jsDoc工具自动提取成文档,在文档里这些虚拟出来的类就像真的类一样五脏俱全了。
Deferred对象
另一个API设计特点就是Deferred对象的广泛使用。Dojo里的Deferred是基于MochiKit实现稍加改进而成的,而后者则是受到 python的事件驱动网络工具包Twisted里同名概念的启发。概括来说的话,这个对象的作用就是将异步IO中回调函数的声明位置与调用位置分离,这样在一个异步IO最终完成的地方,开发人员可以简单地说“货已经到了,想用的可以来拿了”,而不用具体地指出到底该调用哪些回调函数。这样做的好处是让异步IO的写法和同步IO一样(对数据的处理总是在取数据函数的外面,而不是里面),从而简化异步编程。
具体做法是,异步函数总是同步地返回一个代理对象(这就是Deferred对象),可以将它看做你想要的数据的代表,它提供一些方法以添加回调函数,当数据可用时,这些回调函数(可以由很多个)便会按照添加顺序依次执行。如果在取数据过程中出现错误,就会调用所提供的错误处理函数(也可以有很多个);如果想要取消这个异步请求,也可通过Deferred对象的cancel方法完成。
dojo.Deferred的核心方法如下:
then(callback, errback); //添加回调函数 callback(result); //表示异步调用成功完成,触发回调函数 errback(error); //表示异步调用中产生错误,触发错误处理函数 cancel(); //取消异步调用
Dojo还提供了一个when方法,使同步的值和异步的Deferred对象在使用时写法一样。例如:
//某个工具函数的实现 var obj = { getItem: function(){ if(this.item){ return this.item; //这里同步地返回数据 }else{ return dojo.xhrGet({ //这里返回的是Deferred对象 url: "toGetItem.html", load: dojo.hitch(this, function(response){ this.item = response; return response; }) }); } } }; //用户代码 dojo.when(obj.getItem(), function(item){ //无论同步异步,使用工具函数getItem的方式都一样 });
在函数闭包的帮助下,Deferred对象的创建和使用变得更为简单,你可以轻易写出一个创建Deferred对象的函数,以同步的写法做异步的事。例如写一个使用store获取数据的函数:
var store = new dojo.data.QueryReadStore({...}); function getData(start, count){ var d = new dojo.Deferred(); //初始化一个Deferred对象 store.fetch({ start: start, count: count, onComplete: function(items){ //直接取用上层闭包里的Deferred对象 d.callback(items); } }); return d; //把它当做结果返回 }
用dojo.io.script跨域
dojo.xhr* 只是XmlHttpRequest对象的封装,由于同源策略限制,它不能发跨域请求,要跨域还是需要动态创建