本文转自:
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对象的封装,由于同源策略限制,它不能发跨域请求,要跨域还是需要动态创建<script>标签。Dojo 没有像JQuery一样把所有东西都封装在一起(JQuery的ajax()方法可以跨域,当然用的是JSONP,所以它不敢把自己称为xhr),而是坚持一个API只干一件事情。毕竟在大部分应用中,同域请求比跨域请求多得多,如果一个应用不需要跨域,就没必要加载相关代码。因此与xhr不同,dojo 的跨域请求组件不在基本库,而在核心库,需要require一下才能使用:
dojo.require("dojo.io.script");
这个包里面基本上只需要用到一个函数:dojo.io.script.get()。它也返回Deferred对象,并接受类型为 dojo.io.script.__ioArgs的散列参数。受益于虚拟参数类,我们不用从头开始学习这个参数,它继承了dojo.__IoArgs,因此和dojo.xhr*系列的参数大同小异。唯一需要注意的是handleAs在这里无效了,代之以jsonp或者checkString。
前者用于实现JSONP协议,其值由服务器端指定,当script标签加载后就按照JSONP协议执行这个函数,然后Dojo会自动介入,负责把真正的数据传给load函数。需要指出的是在Dojo1.4以前,这个参数叫callbackParamName,冗长但意义明确。毕竟Dojo太早了,它成型的时候(2005)JSONP这个词才刚出现不久。现在callbackParamName还是可用的(为了向后兼容),不过处于deprecated状态。
下面的例子从flickr获取feed数据:
dojo.io.script.get({ url: "http://www.flickr.com/services/feeds/photos_public.gne", jsonp: "jsoncallback", //由flickr指定 content: {format: "json"}, load: function(response){ console.log(response); return response; }, error: function(response){ console.log(response); return response; } });
与jsonp不同,checkString参数专门用于跨域获取javascript代码,它其实是那段跨域脚本里的一个有定义的变量的名字,Dojo会用它来判断跨域代码是否加载完毕,配合前面提到的timeout机制就能实现有效的超时处理。
dojo.io.script.get({ url: "http://......", //某个提供脚本的URL checkString: "obj", load: function(response){ //脚本加载完毕,可以直接使用其中的对象了,如obj。 Return response; } });
用dojo.io.iframe传数据
dojo.io 包里还有一个工具就是iframe,常用于以不刷新页面的方式上传或下载文件。这个很经典的Ajax技巧在Dojo里就是一句 dojo.io.iframe.send({...})。这个函数接受dojo.io.iframe.__ioArgs,相比 dojo.__IoArgs,它只多了一个method参数,用于指定是用GET还是POST(默认)方法发送请求。下面的例子就实现了通过无刷新提交表单来上传文件:
dojo.io.iframe.send({ form: "formNodeId", //某个form元素包含本地文件路径 handleAs: "html", //服务器将返回html页面 load: onSubmitted, //提交成功 error: onSubmitError //提交失败 });
目前send函数的handleAs参数支持html, xml, text, json, 和javascript五种响应格式。除了html和xml之外,使用其他格式有一个比较特别的要求,就是服务端返回的响应必须具有以下格式:
<html> <head></head> <body> <textarea>真正的响应内容</textarea> </body> </html>
这是因为服务器返回的东西是加载在iframe里的,而只有html页面才能在任何浏览器里保证成功加载(有个DOM在,以后取数据也方便)。加一个<textarea>则可以尽量忠实于原始文本数据的格式,而不会受到html的影响。
试试RPC(远程过程调用)
如果dojo.xhr*函数以及Deferred机制仍然无法避免代码的混乱,那RPC可能就是唯一的选择了。dojo.rpc包提供了基于“简单方法描述”语言(SMD)的RPC实现。SMD的原理类似于WSDL(Web服务描述语言),不过是基于JSON的,它定义了远程方法的名称、参数等属性,让 Dojo能创建出代理方法以供调用。
Dojo提供了两种方式实现rpc:XHR和JSONP,分别对应dojo.rpc.JsonService类和dojo.rpc.JsonpService类,用户可以根据是否需要跨域各取所需。
一个简单的例子:
var smdObj = { serviceType: "JSON-RPC", serviceURL: "http://...." methods: [ {name: "myFunc", parameters: []} ] }; var rpc = new dojo.rpc.JsonpService(smdObj); //传入SMD var result = rpc.myFunc(); //直接调用远程方法,返回Deferred对象 dojo.when(result, function(result){ //得到结果 });
SMD还没有一个被广泛认可的官方标准,一直以来都是Dojo社区领导着它的发展,以后这个模块也有可能发生改变,但整个RPC基本的框架会保持稳定。
结语
Ajax请求这个主题太大,本文只能挂一漏万地介绍一点dojo在这方面设计和实现的皮毛,包括基本XHR请求、动态script、iframe请求、以及RPC,并特别强调了几个有Dojo特色的设计,如timeout机制、虚拟参数类、Deferred对象等。
Dojo由Ajax领域的先驱们写就,相信从它的代码中我们一定能学到更多的东西。
参考
- Dojo中文博客
- Dojo权威指南, Mattbew A. Russell 著, 李松峰,李丽 译, O'Reilly, 2009.4
- 精通Dojo, Rawld Gill,Craig Riecke,Alex Russell 著, DOJO中国 译, 人民邮电出版社, 2009.9
- XHR Plugins with Dojo using handleAs, nroberts, http://www.sitepen.com/blog/2008/04/14/xhr-plugins-with-dojo-using-handleas/, 2008.4
- Robust Promises with dojo.Deferred 1.5, Kris Zyp, http://www.sitepen.com/blog/2010/05/03/robust-promises-with-dojo-deferred-1-5/, 2010.5