在任何浏览器上方便地实现 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 对象的封装,由于同源策略限制,它不能发跨域请求,要跨域还是需要动态创建
评论