技术开发 频道

详解Ajax中XMLHttp请求及利弊分析

{$PageTitle=绠€浠媫绠€浠?/string>{$PageTitle=绠€浠媫绠€浠?/string>{$PageTitle=绠€浠媫绠€浠?/string>{$PageTitle=绠€浠媫绠€浠?/string>简介
当Microsoft开始在Internet Explorer 5.0中引入对XML基本的支持时,也引入了一个称为MSXML的ActiveX库,此库中的一个对象很快便成为广为人知的——XMLHttp。
XMLHttp对象最初是为了让开发者能在应用程序中的任何地方都能初始化HTTP请求而创造出来的,这些请求倾向于返回XML,所以XMLHttp对象提供了一个非常简单的方法用于以XML文档的方式访问所需信息。由于从一开始它就是一个ActiveX控件,因此XMLHttp不但可用于网页,也可应用于任何基于Windows的应用程序;然而,显然它在互联网上的受欢迎程序,要大大超出在桌面程序方面。
为紧跟潮流,Mozilla也在它的浏览器Firefox中模仿了XMLHttp功能,但不久后,Safari(从版本1.2开始)和Opera(版本7.6)浏览器也相继模仿了Mozilla的功能实现。今天,这四大浏览器都已在某种程度上支持XMLHttp(Safari和Opera在功能实现上仍不完整,除了GET和POST,不支持其他类型的请求)。

创建一个XMLHttp对象
显然,使用一个XMLHttp对象的第一步,是要创建它,因为Microsoft对此的实现是ActiveX控件,所以你也必须在JavaScript中使用ActiveXObject的所有者类,并传递给它一个XMLHttp控件的签名:

var oXmlHttp = new ActiveXObject("Microsoft.XMLHttp");

上述代码创建了XMLHttp对象的第一个版本(其与IE 5.0一同发布),问题是,随着后续MSXML库的发布,也发布了好几个不同的新版本,每次发布都带来了更好的稳定性与更快的速度,所以,必须要确定你使用的是用户电脑上可用的最新版本。每个版本的签名如下:

Microsoft.XMLHttp
MSXML2.XMLHttp
MSXML2.XMLHttp.3.0
MSXML2.XMLHttp.4.0
MSXML2.XMLHttp.5.0

不幸的是,确定可用非常好的版本唯一的方法,是依次尝试创建它们。因为其本身为一个ActiveX控件,任何创建对象上的失败,都会抛出一个错误,这意味着你必须在一个try…catch块中编写这些代码,如下:

function createXMLHttp() { var aVersions = [ "MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0","MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp","Microsoft.XMLHttp" ]; for (var i = 0; i < aVersions.length; i++) { try { var oXmlHttp = new ActiveXObject(aVersions[i]); return oXmlHttp; } catch (oError) { //Do nothing } } throw new Error("未安装MSXML!"); }
函数createXMLHttp()中保存了一个XMLHttp签名的数组,经常使用的靠前,并遍历此数组以尝试用每个签名创建一个XMLHttp对象。如果创建失败,catch语句将会捕捉到一个JavaScript错误,并尝试下一个签名。当对象被创建后,它将被返回给调用者,如果函数结束时仍没有创建一个XMLHttp对象,将会抛出一个错误指出创建失败。
幸好,在以上浏览器中创建一个XMLHttp对象还算是比较容易的,Mozilla Firefox、Safari和Opera都可以使用下面这行代码:

var oXmlHttp = new XMLHttpRequest();
自然地,它以一种跨浏览器的方式帮助开发者创建XMLHttp对象。你还可以修改前面定义过的createXMLHttp()函数,生成一个如下的函数:

function createXMLHttp() { if (typeof XMLHttpRequest != "undefined") { return new XMLHttpRequest(); } else if (window.ActiveXObject) { var aVersions = [ "MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0","MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp","Microsoft.XMLHttp" ]; for (var i = 0; i < aVersions.length; i++) { try { var oXmlHttp = new ActiveXObject(aVersions[i]); return oXmlHttp; } catch (oError) { //Do nothing } } } throw new Error("不能创建XMLHttp对象!"); }
现在,这个函数先检查是否定义了一个XMLHttpRequest类(使用typeof操作符),如果类XMLHttpRequest存在,它将被用于创建XMLHttp对象;否则,函数检查ActiveXObject类是否存在,如果存在,就执行一遍为IE创建XMLHttp对象相同的过程。如果以上两个测试都失败了,就会抛出一个错误。
另一种创建跨浏览器XMLHttp对象的方法,就是使用已为跨浏览器编写的类库:zXml。它由两位作者编写,可从http://www.nczonline.net/downloads/处下载,库中定义了一个创建XMLHttp对象的单一函数:

var oXmlHttp = zXmlHttp.createRequest();

函数createRequest与zXml库本身,将会贯穿全文,以帮助处理有关跨浏览器的Ajax技术。

在创建完一个XMLHttp对象之后,便可准备从JavaScript中发起HTTP请求。首先调用open()方法,由其完成对象的初始化,此方法接受以下三个参数:

1、 请求类型:指示某种请求类型的字符串,GET或POST(这也是目前所有浏览器都支持的类型)。
2、 URL:请求将发送到的某URL。
3、 异步(Async):一个布尔值,由其决定请求是否异步。

最后一个代表异步的参数,在此非常重要,因为它控制了JavaScript怎样执行请求。当设置为true时,请求被异步发送,JavaScript代码不会等待响应而继续执行,必须设置一个事件处理函数以监视对请求的响应。如果设置为false,请求被同步发送,而JavaScript会在继续执行代码之前等待来自服务器的响应;这意味着如果响应需要很长时间,那么用户将不能操作浏览器,直至响应完成。因此,开发Ajax应用程序最好的方法是,对数据的常规返回操作,尽量使用异步请求,而同步请求用于发送和接收与服务器之间的短小信息。
如对某一info.txt文件的异步GET请求,代码如下:

var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("get", "info.txt", true);

请注意此处的第一个参数,也就是请求类型,即使在技术层面要求请求类型全部为大写字符,但此处使用小写也是没关系的。 接下来,你需要定义一个onreadystatechange事件处理函数,XMLHttp对象有一个名为readyState的属性,当发送请求或收到响应时,此属性值会改变。以下是属性readyState可取的五个值:

0(未初始化):对象已创建,但还未调用open()方法。
1(正在加载):已调用open()方法,但请求还未发送。
2(已加载):请求已经发送。
3(相交互):已接收到一部分响应。
4(完成):所有数据已收到,连接已经关闭。

在每次readystate属性值变化时,都会引发readystatechange事件,并调用onreadystatechange事件处理函数。因为各浏览器实现之间的差异,在跨浏览器开发时唯一可靠的readystate值为0、1和4。在大多数情况下,当请求被返回时,只需检查此值是否为4:

var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("get", "info.txt", true); oXmlHttp.onreadystatechange = function () { if (oXmlHttp.readyState == 4) { alert("得到响应!"); } };
最后一步是调用send()方法,实际上是由它来发送请求的。这个方法只接受单一的参数——一个代表请求体的字符串。如果请求不需要一个请求体(比如说:GET请求就不需要),就必须传递给它一个null值:

var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("get", "info.txt", true); oXmlHttp.onreadystatechange = function () { if (oXmlHttp.readyState == 4) { alert("得到响应!"); } }; oXmlHttp.send(null);
大体流程是,发送请求后,当收到响应,将会显示一个提示框,但仅仅显示一条请求已被接收的消息是远远不够的,XMLHttp的真正威力在于,不但可以让你访问到返回的数据,还可以访问到响应状态和响应头部信息。
为得到从请求返回的数据,可使用responseText和responseXML属性;属性responseText返回一个包含请求体的字符串,而responseXML则是一个XML文档对象,只用于返回数据中包含有text/xml内容的情况。因此,为取得info.txt中的文本,必须进行以下调用:

var sData = oXmlHttp.responseText;
注意,只有文件存在,并且没有错误发生时,才会返回info.txt中相应的文本。但如果info.txt文件不存在,则responseText将返回一个服务器404错误信息,幸好,还有一个方法可以确定是否有错误发生。
属性status中包含有响应返回的HTTP状态代码,而statusText是关于状态的文本描述(如:OK或者Not Found)。使用这两个属性,你可以确认所接收到的数据是实际想要的数据,或告诉用户为什么数据没有收到:

if (oXmlHttp.status == 200) { alert("返回的数据为:" + oXmlHttp.responseText; } else { alert("错误为:" + oXmlHttp.statusText; }
通常来说,必须保证响应的status(状态码)为200,以表明请求完全成功,有时即使服务端有错误发生,属性readState仍会被设为4,所以仅仅检查此值还是不够的。在这个例子中,如果status值为200,将显示responseText的内容,否则,就显示一个出错信息。
在Opera中,仍未实现statusText属性,在其他的浏览器上,有时还会返回一个不精确的描述信息,所以,不能仅仅依靠statusText来判断是否有错误发生。
正如前面所提到的,我们还可以访问到响应头部信息,在此使用getResponseHeader()方法,并传递给它一个你想要的头部名,来得到一个特定的头部值。而当中最有用的一个响应头部信息是Content-Type,它将告诉你正在被发送的数据类型:

var sContentType = oXmlHttp.getResponseHeader("Content-Type"); if (sContentType == "text/xml") { alert("接收到的内容为XML!"); } else if (sContentType == "text/plain") { alert("接收到的内容为纯文本!"); } else { alert("无法判断的内容!"); }
这段代码检查响应中的内容类型,并用一个对话框来显示返回的数据类型。一般来说,从服务器接收到的数据,只会是XML(内容类型为text/xml)或者纯文本(内容类型为text/plain),因为这些类型最容易与JavaScript协同工作。
如果你更想看到从服务器返回的所有头部信息,那么可用getAllResponseHeaders()方法,其将返回一个包含所有头部信息的字符串,由换行符(JavaScript中表示为\n)或回车换行符(JavaScript中表示为\r\n)进行分隔,可以像如下所示对个别的头部信息进行处理:

var sHeaders = oXmlHttp.getAllResponseHeaders(); var aHeaders = sHeaders.split(/\r?\n/); for (var i=0; i < aHeaders.length; i++) { alert(aHeaders[i]); }
这个例子使用JavaScript的split()字符串方法,并传递给它一个正则表达式(由它来匹配回车换行或仅仅是换行),把包含头部信息的字符串分离到一个数组中。现在,你可以遍历这些头部信息,做一些想做的事了。别忘了,数组aHeaders中每一个字符串的形式为:(头部名:值)。
在头部信息被发送出去之前,也可以对头部信息进行修改。你也许想要表明你正在发送的数据内容是什么类型,或者你想要发送一些服务器可能需要与请求一并处理的其他额外数据,要这样做的话,在调用send()之前,必须先调用setRequestHeader()方法:

var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("get", "info.txt", true); oXmlHttp.onreadystatechange = function () { if (oXmlHttp.readyState == 4) { alert("得到响应!"); } }; oXmlHttp.setRequestHeader("myheader", "myvalue"); oXmlHttp.send(null);
在以上代码中,一个名为myheader的头部名在发送之前,被加入到请求中,在此是以myheader:myvalue的形式加入到默认头部信息中的。
由此,就可以处理异步请求了,而异步请求对大多数情况都是适用的。发送异步请求意味着你不需要分配一个onreadstatechange事件处理函数,因为当send()方法返回时,就会收到响应。所以,代码可以这样写:
var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("get", "info.txt", false); oXmlHttp.send(null); if (oXmlHttp.status == 200) { alert("返回的数据为:" + oXmlHttp.responseText; } else { alert("错误为:" + oXmlHttp.statusText; }
异步发送请求(设置open()的第三个参数为false),可以使你在调用send()之后,就立即开始评估响应,如果你想让用户等待响应,或者你只期望收到一小点数据(例如,小于1k),那就另当别论了,但在一般数据量或大数据量的情况中,最好都使用异步调用。

现在,让我们来回顾一下GET示例中的隐藏框架(frame),来看一下使用XMLHttp,是怎样改善处理效率的。第一个目标是GetCustomerDate.php,在对其进行修改后,从一个HTML页变为仅是返回一个HTML代码段,以下是精简过的代码:

<?php header("Content-Type: text/plain"); $sID = $_GET["id"]; $sInfo = ""; $sDBServer = "your.databaser.server"; $sDBName = "your_db_name"; $sDBUsername = "your_db_username"; $sDBPassword = "your_db_password"; $sQuery = "Select * from Customers where CustomerId=".$sID; $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword); @mysql_select_db($sDBName) or $sInfo="不能打开数据库!"; if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) { $aValues = mysql_fetch_array($oResult,MYSQL_ASSOC); $sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />". $aValues['City']."<br />".$aValues['State']."<br />". $aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />". "<a href=\"mailto:".$aValues['E-mail']."\">". $aValues['E-mail']."</a>"; } else{ $sInfo = "ID为$sID的客户不存在!"; } mysql_close($oLink); echo $sInfo; ?>

正如大家所看到的,在页面中没有明显可见的HTML或JavaScript调用,所有的主逻辑保持一致,但加入了两行额外的PHP代码。第一处是在开始处,也就是调用header函数来设置页面的内容类型,即使页面将返回一个HTML代码段,最好也把内容类型设为纯文本(text/plain),因为它不完全是一个HTML页面(因此也不能验证为HTML)。在发送非HTML页面给浏览器时,都应设置好内容类型;第二处是在结尾处,使用echo命令把$sinfo输出到流中。

在主页中——也就是用户访问的第一页,基本设置如下所示:

<p>Enter customer ID number to retrieve information:</p> <p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p> <p><input type="button" value="Get Customer Info" onclick="requestCustomerInfo()" /></p> <div id="divCustomerInfo"></div>

而之前的requestCustomerInfo()函数创建了一个隐藏的iframe,但现在作出修改以使用XMLHttp:

function requestCustomerInfo() { var sId = document.getElementById("txtCustomerId").value; var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("get", "GetCustomerData.php?id=" + sId, true); oXmlHttp.onreadystatechange = function () { if (oXmlHttp.readyState == 4) { if (oXmlHttp.status == 200) { displayCustomerInfo(oXmlHttp.responseText); } else { displayCustomerInfo("错误为:" +oXmlHttp.statusText); } } }; oXmlHttp.send(null); }

请注意,函数以同样的方式开始,即取回用户输入的ID,接着,使用zXml库创建了一个XMLHttp对象,调用open()方法之后,为GetCustomerData.php指定了一个异步的GET请求(其在查询字符串中加入了前述的ID);之后,赋值了一个事件处理函数,除了检查readyState是否为4之外,还要检查请求的status。如果请求成功(status为200),将传递响应体(通过responseText得到)调用displayCustomerInfo()函数;如果有错误发生(status不是200),将会把错误信息传递给displayCustomerInfo()。
在此例和隐藏框架(frame/iframe)示例之间,有几个不同点。首先,在主页之外,不需要JavaScript代码,这点尤其重要,因为当你需要把代码保存在两个不同的地方时,有可能造成兼容性问题,如在基于框架的示例中,是依靠显示页与隐藏框架中各自不同的脚本来相互通讯的,而通过修改GetCustomerInfo.php来返回感兴趣的数据,就已经消除了用JavaScript在两者之间调用的潜在问题。第二个不同之处在于,现在更容易知道,在执行请求期间,是否有问题发生;在前一个例子中,没有在请求处理过程中识别和响应服务器错误的机制,而使用XMLHttp后,所有的服务器错误信息都将展现给作为开发者的你,使你能够只把有意义的错误信息回馈给用户,在大多数时候,对于页内的HTTP请求,XMLHttp都是比隐藏框架更“优雅”的解决方案。


现在,在看过XMLHttp是怎样简化GET请求之后,再来看一下POST请求吧。首先,需要对SaveCustomer.php作出与GetCustomerInfo.php同样的修改,也就是说需要删除无关的HTML与JavaScript代码,加入内容类型信息,并输出相应的文本:

<?php header("Content-Type: text/plain"); $sName = $_POST["txtName"]; $sAddress = $_POST["txtAddress"]; $sCity = $_POST["txtCity"]; $sState = $_POST["txtState"]; $sZipCode = $_POST["txtZipCode"]; $sPhone = $_POST["txtPhone"]; $sEmail = $_POST["txtEmail"]; $sStatus = ""; $sDBServer = "your.database.server"; $sDBName = "your_db_name"; $sDBUsername = "your_db_username"; $sDBPassword = "your_db_password"; $sSQL = "Insert into Customers (Name,Address,City,State,Zip,Phone,`E-mail`) ". " values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'". ", '$sPhone', '$sEmail')"; $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword); @mysql_select_db($sDBName) or $sStatus = "不能打开数据库!"; if($oResult = mysql_query($sSQL)) { $sStatus = "加入的客户;客户ID为".mysql_insert_id(); } else { $sStatus = "在插入操作时有错误发生;客户未保存!"; } mysql_close($oLink); echo $sStatus; ?>
在以上代码中,函数header()的调用是为了设置内容类型,而echo是为了输出$sStatus。
在主页中,用于登记新客户信息的一个简单页面如下所示:
<form method="post" action="SaveCustomer.php" onsubmit="sendRequest(); return false"> <p>Enter customer information to be saved:</p> <p>Customer Name: <input type="text" name="txtName" value="" /><br /> Address: <input type="text" name="txtAddress" value="" /><br /> City: <input type="text" name="txtCity" value="" /><br /> State: <input type="text" name="txtState" value="" /><br /> Zip Code: <input type="text" name="txtZipCode" value="" /><br /> Phone: <input type="text" name="txtPhone" value="" /><br /> E-mail: <input type="text" name="txtEmail" value="" /></p> <p><input type="submit" value="Save Customer Info" /></p> </form> <div id="divStatus"></div>
你可能已经留意到了,onsubmit事件处理函数现在改为调用sendRequest()函数(尽管事件处理函数仍返回false以防止实际的页面提交)。这个方法首先聚集POST请求所需的数据,接着创建XMLHttp对象并发送出去。数据必须作为一个查询字符串,以如下格式的发送:

name1=value1&name2=value2&name3=value3

为防止传输过程中出现数据丢失,其中每一个参数的name和value必须经过URL编码,而JavaScript提供了一个名为encodeURLComponent()的内置函数,可用于进行这种形式的编码。为创建这个字符串,还需要遍历所有的页面字段,取出name和value并进行编码,如下的getRequestBody()函数便是进行这种操作:

function getRequestBody(oForm) { var aParams = new Array(); for (var i=0 ; i < oForm.elements.length; i++){ var sParam = encodeURIComponent(oForm.elements[i].name); sParam += "="; sParam += encodeURIComponent(oForm.elements[i].value); aParams.push(sParam); } return aParams.join("&"); }

函数假定你用一个页面的引用作为参数,创建了一个数组(aParams)以保存每一对单独的name-value,接下来,遍历完页面中的元素,构建一个字符串,并把它存入sParam,而sParam将会在随后添加到数组中;这样做的目的是为了防止多个字符串之间的级联,而其将会在某些浏览器中导致代码执行缓慢。最后一步是在数组上调用join(),并传递给它一个与符号(&),通过这个与符号,有效地结合了所有的name-value,以正确的格式创建出一个单一的字符串。

在大多数浏览器中,字符串的级联是一个开销巨大的处理过程,因为字符串是不可变的,这意味着一旦创建,它们不能改变自身的值,因此,级联两个字符串将导致创建一个新的字符串,并把这两个字符串的内容复制进去,而此过程的周而复始将严重拖慢系统的速度,因为这个原因,最好少用字符串的级联操作,而使用数组的join()方法去处理较长的字符串。
以下的sendRequest()函数调用getRequestBody()并设置好请求:

function sendRequest() { var oForm = document.forms[0]; var sBody = getRequestBody(oForm); var oXmlHttp = zXmlHttp.createRequest(); oXmlHttp.open("post", oForm.action, true); oXmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); oXmlHttp.onreadystatechange = function (){ if (oXmlHttp.readyState == 4){ if (oXmlHttp.status == 200){ saveResult(oXmlHttp.responseText); } else { saveResult("An error occurred: " + oXmlHttp.statusText); } } }; oXmlHttp.send(sBody); }

如同前一个示例一样,此函数中的第一步是得到对页面的一个引用,并把它存到一个变量中(oForm);接着,生成请求体存在sBody中,并创建和设置好XMLHttp对象。注意此处open()中现在已用post代替了get,且第二个参数设置为oForm.action(正是它,所以此脚本才可用于多页面中)。你可能也注意到了,此处也设置了一个请求信息头,当一个页面从浏览器post到服务器时,由它将请求的内容类型设置为application/x-www-form-urlencoded,而大多数的服务器端语言都会查找这条编码,以正确解析传入的POST数据,所以设置它是非常重要的。
而onreadystatechange事件处理函数与GET示例中的是非常类似的,唯一的不同是调用saveResult()而不是displayCustomerInfo();上例的最后一行非常重要,当字符串sBody传递给send()时,它也将成为请求体的一部分,这实际上模仿了浏览器行为,因此服务器端逻辑也会工作正常。

XMLHttp的有利与不利之处

毫无疑问,用于“客户——服务器”通讯的XMLHttp,相比隐藏框架,优势非常明显,编写的代码更加“干净”,另外也不像隐藏框架那样使用大量的回调函数,现在,代码表现的意图清晰易懂。还可以像访问HTTP状态码一样,访问到请求和响应头部信息,让你自己来判断请求是否成功。
不利的方面是,不同于隐藏框架,现在没有函数调用的浏览器历史记录,而“前进”与“后退”按钮也不再与XMLHttp请求有关联,等于是抛弃了这两个按钮,正因为此,许多的Ajax程序都会混合使用XMLHttp与隐藏框架来创建真正实用的界面。
另一个不利方面——在此只关系到Internet Explorer,现在是必须依靠ActiveX控件,如果用户设置了一个特定的安全区域,不允许执行ActiveX控件,你就不可能访问到XMLHttp对象,如果这样,那就不得不使用隐藏框架了。

0
相关文章