在使用应用的时候,特别是独占式应用,作为工作流的一部分,用户会持续地和应用进行交互。第1章中讨论了保持应用响应能力的重要性。如果执行一个长的后台任务使得界面上的一切东西都被锁住,那么就打断了用户的使用。我们讨论了使用异步方法调用的优点,它可以在执行这样的长任务时改善用户界面的响应能力。我们注意到,因为存在网络延迟,那些发到服务器的调用都应该被当作长任务来处理。在基本的HTTP请求—响应模型下,不大可能做到这件事。传统的Web应用依赖于整个页面的重新加载,每一个发到服务器的调用会频繁地打断用户的使用。
我们不得不接受文档请求被阻塞直到服务器返回响应这样的现实。尽管如此,我们还是有很多办法,使得服务器请求对于用户来说看起来像是异步的,这样用户仍然可以继续工作。最早尝试提供这种后台通信能力的是使用IFrame。后来,XMLHttpResuest对象提供了更加清晰和强大的功能。我们在这里考察一下这两个技术。
2.5.1 IFrame
在第4版的Netscape Navigator和微软IE浏览器中,DHTML为Web页面引入了灵活的、可编程的布局。作为老的HTML帧集的自然扩展,出现了IFrame。I代表的是内嵌(inline),意味着它是另外一个文档布局的一部分,而不是像在帧集中一样与之并列。一个IFrame表现为DOM树的一个元素,这意味只要页面可见,我们就可以移动IFrame、改变它的大小或者将它完全隐藏起来。关键的突破在于人们意识到可以设置一个IFrame的样式,使得它完全不可见。这使得不干扰用户体验,以后台方式获取数据成为了可能。突然之间,我们有了一种与服务器进行异步通信的机制,虽然这不过是一种hack式的临时解决方案。图2-5展示了这种方法背后的事件顺序。

图2-5 Web页面中异步通信的事件顺序。用户操作触发了一个隐藏的请求对象(一个IFrame或者XMLHttpRequest对象)向服务器发送一个异步调用。方法非常迅速地返回,只会将用户界面阻塞很短的时间(在图中用阴影区域的高度来表示)。在一个回调函数中解析服务器的响应,并据此更新用户界面
和其他DOM元素一样,IFrame可以在页面的HTML中声明,也可以使用document. createElement()通过程序生成。在一种简单的情况下,我们仅仅想要使用一个不可见的IFrame来加载数据。我们可以将这个IFrame作为文档的一部分来声明,然后使用document. getElementById()获得一个它的引用,如代码清单2-6所示。
代码清单2-6 使用IFrame


因为将它的宽度和高度都设置为0像素,这个IFrame实际上是不可见的。我们也可以将样式设置为display:none,但是某些浏览器会据此进行优化,导致实际上不会去加载相关的文档!还要注意的是,我们需要等到文档加载完成之后才能访问IFrame,因此我们在window.onload事件处理函数中调用getElementById()方法。另一种方法是通过程序按需生成IFrame,如代码清单2-7所示。这种方法的额外好处是可以将所有与请求数据相关的代码放在一起,而不是需要在脚本和HTML之间保持DOM节点唯一ID的同步。
代码清单2-7 创建IFrame

与前面的例子相似,仍然使用createElement()和appendChild()来修改DOM。如果我们严格遵循这种方法,随着应用的持续运行,最终会创建出大量的IFrame。我们要么必须在用完的时候销毁这些IFrame,要么就得实现一个类似于对象池(pooling)的机制。
我们在第3章将要介绍到的设计模式能够帮助我们实现健壮的对象池、队列以及其他用来保证大型应用流畅运转的机制,稍后我们会更加深入地探讨这个话题。在此之前,我们将注意力转移到另外一套可以以后台方式向服务器发送请求的技术。
2.5.2 XMLDocument和XMLHttpRequest对象
正如刚才看到的,我们可以使用IFrame以后台方式请求数据,但是这从本质上来说不过是一种hack式的临时解决方案。最初引入IFrame的设计意图是在页面上显示可见的内容,这种用法歪曲了这个意图。在流行的Web浏览器的更新版本中,引入了专门为异步数据传输而设计的对象,我们将会看到,它比IFrame用起来要方便得多。
XmlDocument和XMLHttpResuest对象并不是Web浏览器中DOM的标准扩展,它们只是碰巧得到了多数浏览器的支持。它们的设计目标很明确,就是用来以后台方式获取数据,这使得发出异步调用的业务使用起来非常流畅。两个对象都是源自微软私有的ActiveX组件,可以在IE浏览器中作为JavaScript对象来访问。其他的浏览器则依照相似的功能和API调用实现了自己的原生对象。两个对象执行的功能很相似,不过XMLHttpRequest可以更加精细地对请求进行控制。在本书中,我们将主要使用XMLHttpRequest,在这里我们简要介绍一下XmlDocument,以便你了解这个对象与XMLHttpRequest有哪些不同。代码清单2-8展示了一段简单创建XmlDocument对象的代码。
代码清单2-8 getXmlDocument()函数


在大多数现代浏览器中,这个函数都能返回一个具有相同API的XmlDocument对象,尽管在不同的浏览器中创建文档的方式有很大不同。
这段代码检查文档对象是否支持创建一个原生的XmlDocument对象所需的implementation属性(在最近的Mozilla和Safari浏览器中都可以找到这个属性)。如果没有找到,它将测试浏览器是否支持ActiveX对象(只有微软的浏览器才能够支持),如果支持,它将尝试定位一个合适的对象。这段脚本优先使用较新一些的第二版MSXML库。
注意 检查浏览器的厂商和版本号信息,并且使用这些信息来开发用于不同浏览器的分支代码,是一种很常见的做法。在我们看来,这种做法容易导致错误,因为它无法预期浏览器的未来版本,并且还会将有能力执行这段脚本的浏览器排除在外。在getXmlDocument()函数中,我们没有尽力去猜测浏览器的版本,而是直接检查特定的对象是否可用。这个方法也称作对象检测(object detection),可以更容易地支持浏览器未来的版本,以及那些我们没有明确测试过的不常见的浏览器,通常这会使得代码更加健壮。
代码清单2-9的代码采用与前面的代码类似的方式获得XMLHttpRequest对象,不过略微简单一些。
代码清单2-9 getXmlHttpRequest()函数

同样地,我们使用对象检测来测试是否支持原生的XMLHttpRequest对象,如果不支持,再测试是否支持ActiveX对象。在两者都不支持的浏览器中,我们简单地返回null。如何更加优雅地处理失败的情况呢?这个问题我们留到第6章再来更加详细地探讨。
我们已经创建了向服务器发送请求的对象,下面我们用它来做什么呢?
2.5.3 向服务器发送请求
通过XMLHttpRequest对象向服务器发送请求是一件相当直接的事情。我们需要做的所有事情就是给它传递一个服务器页面的URL,这个页面将生成数据。就像下面这样:

XMLHttpRequest支持大量的HTTP调用语义,包括用来动态生成页面的可选查询字符串参数(你可能已经知道这些CGI参数、Form参数或者ServletRequest参数,取决于服务器端开发背景)。在考察请求对象如何支持这些功能之前,我们先来快速回顾一下HTTP的基础知识。
HTTP快速入门
HTTP对于因特网而言可谓是无处不在,以至于我们通常都对它熟视无睹。在编写传统的Web应用时,我们近距离接触HTTP协议的地方通常是定义一个超链接或者为一个表单设置method属性。而对于Ajax而言,我们可以深入那些协议的底层细节,这使得我们可以做一些惊人的事情。
浏览器和Web服务器之间的HTTP事务包括浏览器发起的一个请求和随后服务器返回的一个响应(其中也包括执行Web开发人员编写的聪明绝顶、激动人心的代码,这一点毫无疑问)。请求和响应本质上来讲都是文本流,客户端和服务器将它们解释为首部信息和紧随其后的主体部分。你可以将首部信息想像为写在信封上的地址栏,而主体部分是信封中的信件。首部信息简单地指示接收方应该如何处理信件的内容。
一个HTTP请求主要由首部信息和可能包含一些数据或参数的主体部分组成。响应则通常包含返回页面的HTML标记。Mozilla浏览器包含了一个很有用的工具,叫做LiveHTTPHeaders(见本章“资源”一节以及附录A)。我们来查看一下在浏览器工作时,那些请求和响应的首部信息的内容。打开Google的首页,看看在底层发生了些什么事情。
我们发送的第一个请求包含有这样的首部信息:
GET / HTTP/1.1
Host: www.google.com
User-Agent: Mozilla/5.0
(Windows; U; Windows NT 5.0; en-US; rv:1.7)
Gecko/20040803 Firefox/0.9.3
Accept: text/xml,application/xml,
application/xhtml+xml,text/html;q=0.9,
text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: PREF=ID=cabd38877dc0b6a1:TM=1116601572
:LM=1116601572:S=GD3SsQk3v0adtSBP
第一行告诉我们使用的是哪个HTTP方法。大部分Web开发者都很熟悉GET,这是用来获取文档的;还有POST,这是用来提交HTML表单的。万维网联盟(W3C)的规约还包括了一些其他的通用方法,包括HEAD,用来获取一个文件的首部信息;PUT,用于向服务器上传文档;DELETE,用来删除服务器上的文档。后续的首部信息是用来进行沟通的,客户端告诉服务器它所能支持的内容类型、字符集等等。因为我之前曾经访问过Google,它还发送了一个cookie,这段简短的消息告诉Google我是谁。
下面显示的是响应的首部信息,也包含了大量的信息:
HTTP/1.x 302 Found
Location: http://www.google.co.uk/cxfer?c=PREF%3D:
TM%3D1116601572:S%3DzFxPsBpXhZzknVMF&prev=/
Set-Cookie: PREF=ID=cabd38877dc0b6a1:CR=1:TM=1116601572:
LM=1116943140:S=fRfhD-u49xp9UE18;
expires=Sun, 17-Jan-2038 19:14:07 GMT;
path=/; domain=.google.com
Content-Type: text/html
Server: GWS/2.1
Transfer-Encoding: chunked
Content-Encoding: gzip
Date: Tue, 24 May 2005 17:59:00 GMT
Cache-Control: private, x-gzip-ok=""
第一行指示了响应的状态。302响应意味着重定向到另外一个页面。服务器还为此次会话发回了另外一个cookie。此外,还声明了响应的内容类型(也就是MIME类型)。这个重定向指令引起客户端发送了一个新的请求,随后得到了第二个响应,带有下列首部信息:
HTTP/1.x 200 OK
Cache-Control: private
Content-Type: text/html
Content-Encoding: gzip
Server: GWS/2.1
Content-Length: 1196
Date: Tue, 24 May 2005 17:59:00 GMT
状态码200表示成功,用以显示的Google首页附加在响应的主体部分。Content-type告诉浏览器,内容的类型是html。
sendRequest()方法包含的第二个和第三个参数都是可选参数,大部分情况下都用不上。默认使用POST方法来获取资源,在请求的主体部分不需要传递任何参数。
清单中的代码对请求进行设置后,会立即将控制权返回给我们,与此同时网络和服务器则忙着执行它们自己的任务。这对于提高响应能力很有好处,但是我们要怎样才能知道请求完成了呢?
2.5.4 使用回调函数监视请求
处理异步通信的第二个部分是在代码中设置一个入口点,以便在调用结束的时候可以获取结果。这通常是通过分配一个回调函数来实现的,也就是说,在未来的某个不确定时刻,当结果返回的时候,将会执行这一段代码。我们在代码清单2-9中看到的window.onload函数就是一个回调函数。
回调函数非常适合用于大多数现代UI工具箱中的事件驱动的编程方法。按下键盘、点击鼠标等等,这些事件都将会在未来某个无法预测的时刻发生,程序员预见到了这一点,并且为这些事件写好了处理函数。在用JavaScript编写用户界面的事件处理代码时,我们将函数分配给onkeypress、onmouseover或者对象上的类似属性。在为服务器请求的回调函数编写代码时,我们见到过称作onload和onreadystatechange的类似属性。
IE和Mozilla都支持onreadystatechange回调函数,所以我们可以放心地使用(Mozilla还支持onload,这更加直接一点,但是它不能像onreadystatechange那样给我们所需要的信息)。一个简单的回调处理函数展示在代码清单2-10。
代码清单2-10 使用回调处理函数
var READY_STATE_UNINITIALIZED=0;
var READY_STATE_LOADING=1;
var READY_STATE_LOADED=2;
var READY_STATE_INTERACTIVE=3;
var READY_STATE_COMPLETE=4;
var req;
function sendRequest(url,params,HttpMethod){
if (!HttpMethod){
HttpMethod="GET";
}
req=getXMLHTTPRequest();
if (req){
req.onreadystatechange=onReadyStateChange;
req.open(HttpMethod,url,true);
req.setRequestHeader
("Content-Type", "application/x-www-form-urlencoded");
req.send(params);
}
}
function onReadyStateChange(){
var ready=req.readyState;
var data=null;
if (ready==READY_STATE_COMPLETE){
data=req.responseText;
}else{
data="loading...["+ready+"]";
}
//... do something with the data...
}
首先,在发送请求之前,我们修改了sendRequest()函数,告诉请求对象使用哪个回调函数。其次,我们定义了这个处理函数,并且给它取了一个缺乏想象力的名字onReadySt- ateChange()。
readyState可以取一系列数值。为了使得代码易于阅读,我们在这里给每一个数值分配了一个有描述性的变量名。目前的代码只对数值4感兴趣,它表明请求已经完成。
要注意的是,我们将请求对象声明为一个全局变量。在这里,这有助于事情保持简单,以便更好地理解XMLHttpRequest对象的工作原理。但是,如果我们试图同时发送多个请求,这样就会带来一些麻烦。如何解决这个问题我们留到3.1节再讨论。现在将这些部分整合起来,从头到尾看看如何处理请求。
2.5.5 完整的生命周期
我们现在已经有了足够的信息,可以完成加载一个文档的全部生命周期,就如代码清单2-11中所示。我们初始化XMLHttpRequest对象,让它加载一个文档,然后使用回调处理函数来异步地对加载过程进行监视。在这个简单的例子中,我们定义了一个称作console的DOM节点,用来输出状态信息,以便得到下载过程的书面记录。
代码清单2-11 完整的使用XMLHttpRequest加载文档的例子
var req=null;
var console=null;
var READY_STATE_UNINITIALIZED=0;
var READY_STATE_LOADING=1;
var READY_STATE_LOADED=2;
var READY_STATE_INTERACTIVE=3;
var READY_STATE_COMPLETE=4;




