本文深入解析 JavaScript 中 Ajax 的核心 XMLHttpRequest 对象。内容涵盖从基础用法到第二级规范的扩展功能,详细讲解 readyStatechange 和 progress 等事件监听机制。重点阐述设置错误、请求错误及应用层错误的三种处理策略,并介绍如何通过自定义标头覆盖 HTTP 方法及禁用缓存,帮助开发者构建稳健的异步 Web 应用。
Ajax 与 XMLHttpRequest 基础
Ajax 是现代 Web 应用程序开发的关键技术。它允许程序向服务器异步发送和接收数据,随后由 JavaScript 进行解析。Ajax 全称为 Asynchronous JavaScript and XML(异步 JavaScript 与 XML)。
Ajax 的核心规范基于用于建立和发起请求的 JavaScript 对象:XMLHttpRequest。该规范分为两个等级。主流浏览器均实现了第一级,提供基础功能;第二级则在原规范基础上进行了扩展,增加了额外的事件和功能,使其更易于与 form 元素协作,并支持相关规范。
1. Ajax 起步
理解 Ajax 的关键在于掌握 XMLHttpRequest 对象,通过示例代码可以直观了解其用法。以下代码展示了 XMLHttpRequest 对象的基本使用方式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>
</head>
<body>
<div>
<button>apples</button>
<button>cherries</button>
<button>bananas</button>
</div>
<div id="target">
Press a button
</div>
<script type="application/javascript">
var buttons = document.getElementsByTagName("button");
for(var i=0; i<buttons.length; i++){
buttons[i].onclick = handleButtonPress;
}
//脚本会调用此函数以响应 button 控件的 click 事件
function handleButtonPress(e){
//创建一个新的 XMLHttpRequest 对象
var httpRequest = new XMLHttpRequest();
//给 onreadystatechange 事件设置一个事件处理器
httpRequest.onreadystatechange = handleResponse;
//使用 open 方法来指定 HTTP 方法和需要请求的 URL (即告诉 httpRequest 对象你想要做的事)
httpRequest.open("GET", e.target.innerHTML+".html");
//这里没有向服务器发送任何数据,所以 send 方法无参数可用
httpRequest.send();
}
//处理响应
//一旦脚本调用了 send 方法,浏览器就会在后台发送请求到服务器。因为请求是在后台处理的,所以Ajax 依靠事件来通知这个请求的进展情况。
function handleResponse(e){
//当 onreadystatechange 事件被触发后,浏览器会把一个 Event 对象传递给指定的处理函数,target 属性则会被设为与此事件关联的XMLHttpRequest
if(e.target.readyState == XMLHttpRequest.DONE && e.target.status == 200){ //请求成功
document.getElementById("target").innerHTML = e.target.responseText; //显示被请求文档的内容
}
}
</script>
</body>
</html>
此外还有三个辅助文档,结构非常简洁:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Apples</title>
<style>
img { float:left;padding:2px;margin:5px;border: medium double black;background-color: lightgrey; width: 100px;height: 100px;}
</style>
</head>
<body>
<p>
<img src="../img/show-page/img_apples.jpg"/>
Page for apples.
</p>
</body>
</html>
运行效果如下图所示:

当用户点击不同的水果按钮时,浏览器会异步执行请求并获取对应文档,而主页面不会重新加载。这正是典型的 Ajax 行为表现。
Ajax 事件监听机制
2. 使用 Ajax 事件
在建立并探索简单示例后,我们可以深入探讨 XMLHttpRequest 对象支持的功能及其在请求中的应用。切入点在于第二级规范中定义的额外事件:

大多数事件会在请求的特定阶段触发。readystatechange 和 progress 是两个例外,它们可以多次触发以提供进度更新信息。
浏览器在调度这些事件时,对 readystatechange 使用常规的 Event 对象,而对其他事件则使用 ProgressEvent 对象。ProgressEvent 对象包含了 Event 对象的所有成员,并增加了图中介绍的额外成员:

以下代码展示了如何具体使用这些事件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>
<style>
table {margin: 10px;border-collapse: collapse; float: left;}
div{margin: 10px;}
td,th{padding: 4px;}
</style>
</head>
<body>
<div>
<button>apples</button>
<button>cherries</button>
<button>bananas</button>
</div>
<table id="events" border="1"></table>
<div id="target">
Press a button
</div>
<script type="application/javascript">
var buttons = document.getElementsByTagName("button");
for(var i=0; i<buttons.length; i++){
buttons[i].onclick = handleButtonPress;
}
var httpRequest;
function handleButtonPress(e){
clearEventDetails();
httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = handleResponse;
httpRequest.onerror = handleError;
httpRequest.onload = handleLoad;;
httpRequest.onloadend = handleLoadEnd;
httpRequest.onloadstart = handleLoadStart;;
httpRequest.onprogress = handleProgress;
httpRequest.open("GET", e.target.innerHTML+".html");
httpRequest.send();
}
function handleResponse(e){
displayEventDetails("readystate("+httpRequest.readyState+")")
if(e.target.readyState == XMLHttpRequest.DONE && e.target.status == 200){
document.getElementById("target").innerHTML = e.target.responseText;
}
}
function handleError(e){ displayEventDetails("error",e);}
function handleLoad(e){ displayEventDetails("load",e);}
function handleLoadEnd(e){ displayEventDetails("loadend",e);}
function handleLoadStart(e){ displayEventDetails("loadstart",e);}
function handleProgress(e){ displayEventDetails("progress",e);}
function clearEventDetails(){
document.getElementById("events").innerHTML = "<tr><th>Event</th><th>lengthComputable</th><th>loaded</th><th>total</th>";
}
function displayEventDetails(eventName,e){
if(e){
document.getElementById("events").innerHTML
+="<tr><td>"+eventName+"</td><td>"+ e.lengthComputable+"</td><td>"+ e.loaded+"</td><td>"+ e.total+"</td></tr>";
}else {
document.getElementById("events").innerHTML += "<tr><td>"+eventName+"</td><td>NA</td><td>NA</td><td>NA</td></tr>";
}
}
</script>
</body>
</html>
这是前述示例的一个变体,为部分事件注册了处理函数,并在 table 元素中为每个处理事件创建了一条记录。从图中可以看到 Firefox 浏览器触发这些事件的具体情形。

Ajax 错误处理策略
3. 处理错误
在使用 Ajax 时,必须注意两类错误。这两类错误的区别源于观察视角的不同。
第一类错误是从 XMLHttpRequest 对象的角度观察到的问题:某些因素导致请求无法发送到服务器。例如 DNS 无法解析主机名、连接请求被拒绝或 URL 无效。
第二类问题是从应用程序的角度观察到的问题,而非 XMLHttpRequest 对象本身。这类问题发生在请求成功发送至服务器,服务器接收并处理请求生成响应后,但该响应并非期望的内容。例如,当请求的 URL 不存在时,就会发生此类问题。
处理这些错误有三种方式,如下面代码所示:
3.1 处理设置错误
需要处理的第一类问题是向 XMLHttpRequest 对象传递了错误数据,例如格式不正确的 URL。这类错误在生成基于用户输入的 URL 时极易发生。为了模拟此类问题,上述文档中添加了一个标记为 Bad URL(错误的 URL)的按钮。按下该按钮会以以下形式调用 open 方法:
httpRequest.open("GET","http://")
这是一种会阻止请求执行的错误,当 XMLHttpRequest 对象发生此类错误时会抛出异常。因此,需要用 try...catch 语句包裹设置请求的代码,如下所示:
try{
...
httpRequest.open("GET","http://")
...
httpRequest.send();
}catch(error){
displayErrorMsg("try/catch",error.message)
}
catch 子句提供了从错误中恢复的机会。可以选择提示用户输入新值、回退至默认 URL,或直接丢弃该请求。在本例中,仅调用 displayErrorMsg 函数来显示错误消息。
3.2 处理请求错误
第二类错误发生在请求已生成,但其他方面出现错误时。为了模拟此类问题,在示例中添加了一个标记为 Bad Host(错误主机)的按钮。当按下该按钮时,会调用 open 方法访问一个不可用的 URL:
httpRequest.open("GET",http://www.ycdoitt.com/nopage.html)
该 URL 存在两个问题。第一个问题是主机名无法被 DNS 解析,因此浏览器无法生成服务器连接。这个问题直到 XMLHttpRequest 对象开始生成请求时才会变得明显,因此它会以两种方式发出错误信号。如果注册了 error 事件的监听器,浏览器会向监听函数发送一个 Event 对象。以下是示例中使用的函数:
function handleError(e){
displayErrorMsg("Error event",httpRequest.status + httpRequest.statusText);
}
当此类错误发生时,能从 XMLHttpRequest 对象获取的信息程度取决于浏览器。遗憾的是,大多数情况下,status 值为 0,statusText 为空。
第二个问题是 URL 与生成请求的页面具有不同的来源,默认情况下这是不允许的。通常只能向载入脚本的同源 URL 发送 Ajax 请求。浏览器在报告此问题时可能会抛出 Error 或触发 error 事件,不同浏览器的处理方法不尽相同。此外,不同浏览器检查来源的时机不同,这意味着不一定总是能看到浏览器对同一问题的提示。可以使用跨站资源规范(CORS, Cross-Origin Resource Sharing)来绕过同源限制。
3.3 处理应用程序错误
最后一类错误发生在请求成功完成(从 XMLHttpRequest 对象的角度看),但未返回期望数据时。为了制造此类问题,在示例中添加了一个标记为 cucumber 的按钮。按下该按钮会生成类似于 apples、cherries 和 bananas 按钮那样的请求 URL,但服务器上不存在 cucumber.html 这个文档。
这一过程本身没有错误(因为请求已完成),需要根据 status 属性来确定具体情况。当请求某个存在的文档时,会获得 404 状态码,表示服务器无法找到请求的文档。可以看到示例是如何处理 200(表示 OK)以外的状态码的:
if(httpRequest.status == 200){
target.innerHTML = httpRequest.responseText;
}else{
document.getElementById("statusmsg").innerHTML
= "Status:" + httpRequest.status +" "+ httpRequest.statusText;
}
在此示例中,仅简单显示了 status 和 statusText 的值。而在实际应用程序中,需要以有用且有意义的方式进行恢复(例如显示备用内容或警告用户,具体取决于哪种方式更适合应用程序)。
请求标头操作与 HTTP 方法覆盖
4. 获取和设置标头
使用 XMLHttpRequest 对象,可以设置发送给服务器的请求标头(Header),并读取服务器响应中的标头。

4.1 覆盖请求的 HTTP 方法
通常不需要添加或修改 Ajax 请求中的标头。浏览器知道需要发送什么,服务器也知道如何响应。不过,有几种情况例外。第一种是 X-HTTP-Method-Override 标头。
HTTP 标准通常用于在互联网上请求和传输 HTML 文档,它定义了许多方法。大多数人熟悉 GET 和 POST,因为它们使用最广泛。但还存在其他一些方法(包括 PUT 和 DELETE),这些 HTTP 方法用于给向服务器请求的 URL 赋予意义,且这种用法正呈上升趋势。例如,若想查看某条用户记录,可以生成如下请求:
httpRequest.open("GET","http://myserver/records/freeman/adam");
这里仅展示了 HTTP 方法和请求的 URL。要使该请求顺利工作,服务器端必须由应用程序理解该请求,并将其转换为合适的数据以发送回客户端。若想删除数据,可以这样写:
httpRequest.open("DELETE","http://myserver/records/freeman/adam");
此处的关键在于通过 HTTP 方法表达你想让服务器执行的操作,而不是将其编码进 URL。
以这种方式使用 HTTP 方法的问题在于:许多主流 Web 技术仅支持 GET 和 POST,且不少防火墙只允许 GET 和 POST 请求通过。一种规避此限制的惯用做法是使用 X-HTTP-Method-Override 标头来指定想要使用的 HTTP 方法,但形式上发送的是 POST 请求。代码演示如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>
</head>
<body>
<div>
<button>apples</button>
<button>cherries</button>
<button>bananas</button>
</div>
<div id="target">Press a button</div>
<script>
var buttons = document.getElementsByTagName("button");
for(var i = 0; i < buttons.length; i++){
buttons[i].onclick = handleButtonPress;
}
var httpRequest;
function handleButtonPress(e){
httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = handleResponse;
httpRequest.open("GET", e.target.innerHTML+".html");
httpRequest.setRequestHeader("X-HTTP-Method-Override","DELETE");
httpRequest.send();
}
function handleError(e){
displayErrorMsg("Error event",httpRequest.status+httpRequest.statusText);
}
function handleResponse(){
if(httpRequest.readyState == 4 && httpRequest.status == 200){
document.getElementById("target").innerHTML = httpRequest.responseText;
}
}
</script>
</body>
</html>
在此示例中,使用了 XMLHttpRequest 对象上的 setRequestHeader 方法来表明希望该请求以 HTTP DELETE 方法的形式进行处理。请注意,我在调用 open 方法之后才设置了此标头。如果试图在 open 方法之前使用 setRequestHeader 方法,XMLHttpRequest 对象会抛出错误。
PS: 覆盖 HTTP 方法需要服务器端的 Web 应用程序框架能理解 X-HTTP-Method-Override 这一惯例,并且你的服务器端应用程序要设置成能寻找和理解那些用得较少的 HTTP 方法。
4.2 禁用内容缓存
第二个可以添加到 Ajax 请求上的有用标头是 Cache-Control,它在编写和调试脚本时尤其有用。一些浏览器会缓存通过 Ajax 请求获得的内容,在浏览会话期间不会再请求它。对于前例而言,这意味着 apples.html、cherries.html 和 bananas.html 上的改动不会立即反映到浏览器中。以下代码展示了如何设置标头来避免这一情况:
httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = handleResponse;
httpRequest.open("GET", e.target.innerHTML+".html");
httpRequest.setRequestHeader("Cache-Control","no-cache");
httpRequest.send();
设置标头的方式与前例相同,但此次使用的标头是 Cache-Control,期望值为 no-cache。放置该语句后,如果通过 Ajax 请求的内容发生改变,就会在下次请求文档时体现出来。
4.3 读取响应标头
可以通过 getResponseHeader 和 getAllResponseHeaders 方法来读取服务器响应某个 Ajax 请求时发送的 HTTP 标头。在大多数情况下,无需关心标头内容,因为它们是浏览器和服务器之间交互事务的组成部分。以下代码展示了如何使用这些属性:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width,user-scalable=no" name="viewport" />
<meta name="author" content="叶超Luka" />
<meta name="description" content="A simple example" />
<title>Example</title>
<link href="../img/ycdoit.ico" type="image/x-icon" rel="shortcut icon" />
<style>
#allheaders,#ctheader{border: medium solid black;padding: 2px;margin: 2px;}
</style>
</head>
<body>
<div>
<button>apples</button>
<button>cherries</button>
<button>bananas</button>
</div>
<div id="ctheader"></div>
<div id="allheaders"></div>
<div id="target">Press a button</div>
<script>
var buttons = document.getElementsByTagName("button");
for(var i = 0; i < buttons.length; i++){
buttons[i].onclick = handleButtonPress;
}
var httpRequest;
function handleButtonPress(e){
httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = handleResponse;
httpRequest.open("GET", e.target.innerHTML+".html");
httpRequest.setRequestHeader("Cache-Control","no-cache");
httpRequest.send();
}
function handleResponse(){
if(httpRequest.readyState==2){
document.getElementById("allheaders").innerHTML = httpRequest.getAllResponseHeaders();
document.getElementById("ctheader").innerHTML = httpRequest.getResponseHeader("Content-Type");
}else if(httpRequest.readyState == 4 && httpRequest.status == 200){
document.getElementById("target").innerHTML = httpRequest.responseText;
}
}
</script>
</body>
</html>
效果图如下:

根据此图可以看出,开发服务器正在运行的 Web 服务器软件是 IntelliJ IDEA 15.0.4,最后修改 apples.html 文档的时间是 6 月 27 日(尽管屏幕截图拍摄于 7 月 5 日)。