用 Dojo 实现 Ajax 请求:XHR、跨域、及其他

阅读数:7391 2011 年 1 月 18 日 00:00

在任何浏览器上方便地实现 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 领域的先驱们写就,相信从它的代码中我们一定能学到更多的东西。

参考

  1. Dojo 中文博客
  2. Dojo 权威指南, Mattbew A. Russell 著, 李松峰, 李丽 译, O'Reilly, 2009.4
  3. 精通 Dojo, Rawld Gill,Craig Riecke,Alex Russell 著, DOJO 中国 译, 人民邮电出版社, 2009.9
  4. XHR Plugins with Dojo using handleAs, nroberts, http://www.sitepen.com/blog/2008/04/14/xhr-plugins-with-dojo-using-handleas/, 2008.4
  5. 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

感谢张凯峰对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评论

发布