你的位置:首页 > Java教程

[Java教程]【温故而知新


Ajax 是现代Web 应用程序开发的一项关键工具。它让你能向服务器异步发送和接收数据,然后用 Javascript 解析。 Ajax 是 Asynchronous JavaScript and

Ajax 核心规范的名称继承于用来建立和发起请求的 Javascript 对象:

 

1. Ajax起步

Ajax 的关键在于

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <title>使用</title></head><body><div>  <button>apples</button>  <button>bananas</button>  <button>lemons</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){    //创建一个新的     var httpRequest = new //给 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 属性则会被设为与此事件关联的    if(e.target.readyState == && e.target.status == 200){ //请求成功      document.getElementById("target").innerHTML = e.target.responseText; //显示被请求文档的内容    }  }</script></body></html>

当某个按钮被按下后,示例中的脚本会载入另一个HTML文档,并让它称为div元素的内容。其他的文档一共三个,分别对应button元素上的说明标签:apples.html、lemons.html 和 bananas.html。

此例的显示效果如下图所示:

这三个额外的文档非常简单,其中apples.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="../imgs/apple.png"/>  Page for apples.</p></body></html>

随着用户点击各个水果按钮,浏览器会异步执行并取回所请求的文档,而主文档不会被重新加载。这就是典型的 Ajax 行为。

 

2. 使用 Ajax 事件

建立和探索一个简单的示例之后,可以开始深入了解

这些事件大多数会在请求的某一特定时间点上触发。 readystatechange 和 progress 这两个事件是例外,它们可以多次触发以提供进度更新。

调度这些事件时,浏览器会对 readystatechange 事件使用常规的 Event 对象,对其他事件则使用 ProgressEvent 对象。 ProgressEvent 对象定义了 Event 对象的所有成员,并增加了下图中介绍的这些成员:

下面代码展示了如何使用这些事件: 

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <title>使用</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>bananas</button>  <button>lemons</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 = 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 == && 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 浏览器是如何触发这些事件的。

3. 处理错误

使用 Ajax 时必须留心两类错误。它们之间的区别源于视角不同。

第一类错误是从

第二类问题是从应用程序的角度看到的问题,而非

有三种方式可以处理这些错误,如下面代码所示:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <title>处理Ajax错误</title></head><body><div>  <button>apples</button>  <button>bananas</button>  <button>lemons</button>  <button id="badhost">Bad Host</button>  <button id="badurl">Bad URL</button></div><div id="target">Press a button</div><div id="errormsg"></div><div id="statusmsg"></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){    clearMessages();    httpRequest = new = handleResponse;    httpRequest.onerror = handleError;    try{      switch(e.target.id){        case "badhost":          httpRequest.open("GET","http://www.ycdoitt.com/nopage.html")          break;        case "badurl":          httpRequest.open("GET","http://")          break;        default:          httpRequest.open("GET", e.target.innerHTML + ".html")          break;      }      httpRequest.send();    }catch(error){      displayErrorMsg("try/catch",error.message+"("+error.name+")")    }  }  function handleError(e){    displayErrorMsg("Error event",httpRequest.status + httpRequest.statusText);  }  function handleResponse(){    if(httpRequest.readyState == 4){      var target = document.getElementById("target");      if(httpRequest.status == 200){        target.innerHTML = httpRequest.responseText;      }else{        document.getElementById("statusmsg").innerHTML            = "Status:" + httpRequest.status +" "+ httpRequest.statusText;      }    }  }  function displayErrorMsg(src,msg){    document.getElementById("errormsg").innerHTML = src + ": " + msg;  }  function clearMessages(){    document.getElementById("errormsg").innerHTML = "";    document.getElementById("statusmsg").innerHTML = "";  }</script></body></html>

此例演示结果如下:

 

3.1 处理设置错误

需要处理的第一类问题是向

 httpRequest.open("GET","http://")

这是一种会阻止请求执行的错误,而

    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 解析,因此浏览器无法生成服务器连接。这个问题知道

function handleError(e){    displayErrorMsg("Error event",httpRequest.status + httpRequest.statusText);  }

当这类错误发生时,能从

 第二个问题是URL和生成请求的具有不同的来源,在默认情况下这是不允许的。你通常只能向载入脚本的同源URL发送Ajax请求。浏览器报告这个问题时可能会抛出 Error 或者触发error事件,不同浏览器的处理方法不尽相同。不同浏览器还会在不同的时点检查来源,这就意味着不一定总是能看到浏览器对同一问题突出显示。可以使用跨站资源规范(CORS,Cross-Origin Resource Sharing)来绕过同源限制。

 

3.3 处理应用程序错误

最后一类错误发生于请求成功完成(从

这一过程本身没有错误(因为请求已完成),需要根据 status属性来确定发生了什么。当请求某个存在的文档时,会获得404这个状态码,它的意思是服务器无法找到请求的文档。可以看到示例是如何处理200(意思是OK)以外的状态码的:

      if(httpRequest.status == 200){        target.innerHTML = httpRequest.responseText;      }else{        document.getElementById("statusmsg").innerHTML            = "Status:" + httpRequest.status +" "+ httpRequest.statusText;      }

在这个例子中,只是简单的显示了status和statusText的值。而在真正的应用程序里,需要以一种有用且有意义的方式进行恢复(比如显示备用内容或警告用户有问题,具体看哪种更适合应用程序)。

 

4. 获取和设置标头

使用

 

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>设置一个请求标头</title></head><body><div>  <button>apples</button>  <button>bananas</button>  <button>lemons</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 = 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>

在这个例子中,有使用

PS:覆盖HTTP需要服务器端的Web应用程序框架能理解X-HTTP-Method-Override这个惯例,并且你的服务器端应用程序要设置成能寻找和理解那些用的较少的HTTP方法。

 

4.2 禁用内容缓存

第二个可以添加到Ajax请求上的有用标头是Cache-Control,它在编写和调试脚本时尤其有用。一些浏览器会缓存通过Ajax请求所获得的内容,在浏览会话期间不会再请求它。对在前面的例子而言,意味着 apples.html、cherries.html和bananas.html 上的改动不会立即反映到浏览器中。下面代码展示了可以如何设置标头来避免这一点:

    httpRequest = new 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>读取响应标头</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>bananas</button>  <button>lemons</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 = 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 文档的时间是8月17日。

 

5. 生成跨源Ajax请求

默认情况下,浏览器限制脚本只能在它们所属文档的来源内生成Ajax请求。而来源由URL中的协议、主机名和端口号组成。这就意味着当从http://titan 载入了一个文档后,文档内含的脚本通常无法生成对 http://titan:8080 的请求,因为第二个URL的端口号是不同的,所以处于文档来源之外。从一个来源到另一个来源的Ajax请求被称为跨源请求(cross-origin request)。

PS:这一策略的目的是降低跨站脚本攻击(cross-site scripting,简称CSS)的风险,即诱导浏览器(或用户)执行恶意脚本。

这个策略的问题在于它一刀切地禁止了跨源请求。幸好,跨源资源共享(Cross-Origin Resource Sharing,CORS)规范提供了一种合法的方式来生成跨源请求。

作为准备,下面代码展示了一个HTML文档,它包含的脚本会尝试生成跨源请求:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <title>尝试生成跨源请求的脚本</title></head><body><div>  <button>apples</button>  <button>bananas</button>  <button>lemons</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 = handleResponse;    httpRequest.open("GET","http://127.0.0.1:8080/"+ e.target.innerHTML);    httpRequest.send();  }  function handleResponse(){    if(httpRequest.readyState == 4 && httpRequest.status == 200){      document.getElementById("target").innerHTML = httpRequest.responseText;    }  }</script></body></html>

这个例子中的脚本扩展了用户所按按钮的内容,把它附加到 http://127.0.0.1:8080/ 上,然后尝试生成Ajax请求(如 http://127.0.0.1:8080/apples)。这里会从 http://localhost:63342/HTML5/ajax/ajax-06.html 载入此文档,这就意味着脚本正在试图生成一个跨源请求。

脚本尝试连接的服务器运行的是 Node.js。代码如下,把它保存在了一个名为 fruitselector.js 的文件里:

var http = require('http');http.createServer(function (req,res){  console.log("[200] "+req.method+" to "+req.url);  res.writeHead(200,"OK",{    "Content-Type":"text/html"    });  res.write('<html><head><title>Fruit Total</title></head><body>');  res.write('<p>');  res.write('You selectd '+req.url.substring(1));  res.write('</p></body></html>');  res.end();}).listen(8080);

这是一个非常简单的服务器:它根据客户端请求的URL生成一小段HTML文档。距离来说,如果客户端请求了 http://127.0.0.1:8080/apples ,那么客户端就会生成并返回下列HTML文档:

<html><head><title>Fruit Total</title></head><body><p>You selectd apples</p></body></html>

按照现在这个样子,show-ajax-css.html 里的脚本无法从服务器获取它想要的数据。试运行,反映问题如下图所示:

所以,解决办法是为服务器返回浏览器的响应信息添加一个标头,代码如下所示:

var http = require('http');http.createServer(function (req,res){  console.log("[200] "+req.method+" to "+req.url);  res.writeHead(200,"OK",{    "Content-Type":"text/html",    "Access-Control-Allow-Origin":"http://localhost:63342"    });  res.write('<html><head><title>Fruit Total</title></head><body>');  res.write('<p>');  res.write('You selectd '+req.url.substring(1));  res.write('</p></body></html>');  res.end();}).listen(8080);

Access-Control-Allow-Origin 标头指定了某个来源应当被允许对此文档生成跨源请求。如果标头里指定的来源于当前文档的来源匹配,浏览器就会加载和处理该响应所包含的数据。

PS:支持CORS要求浏览器必须在联系服务器和获取响应标头之后应用跨源安全策略,这就意味着即使响应因为缺少必要的标头或指定了不同的域而被丢弃,请求也已被发送过了。这种方式和没有实现CORS的浏览器非常不同,后者只会简单的阻挡请求,不会去联系服务器。

给服务器响应添加这个标头之后,show-ajax-css.html 文档中的脚本就能够请求和接收来着服务器的数据了,如下图所示:

 

5.1 使用 Origin 请求标头

作为CORS的一部分,浏览器会给请求添加一个 Origin标头以注明当前文档的来源。可以通过它来更灵活地设置 Access-Control-Allow-Origin 标头的值,代码如下所示:

var http = require('http');http.createServer(function (req,res){  console.log("[200] "+req.method+" to "+req.url);    res.statusCode = 200;  res.setHeader("Content-Type","text/html");    var origin = req.headers["origin"];  if(origin.indexOf("localhost:63342") > -1){    res.setHeader("Access-Control-Allow-Origin",origin)  }    res.write('<html><head><title>Fruit Total</title></head><body>');  res.write('<p>');  res.write('You selectd '+req.url.substring(1));  res.write('</p></body></html>');  res.end();}).listen(8080);

这里修改了服务器端的脚本,让它只在请求包含Origin标头并且值里有 localhost:63342 时才设置 Access-Control-Allow-Origin 响应标头。这是一种非常粗略的请求来源检查方式,但可以根据具体项目的上下文环境来调整这种方式,使它更精确。

PS:还可以吧 Access-Control-Allow-Origin 标头设置成一个星号(*),意思是允许任何来源的跨源请求。使用这个设置之前应该仔细考虑这么做的安全隐患。

 

5.2 高级CORS功能

CORS规范定义了许多额外的标头,可用于精细化控制跨域请求,包括限制请求能够使用的HTTP方法。这些高级功能需要进行一次预先请求(preflight request),即浏览器先向服务器发送一个请求来确定有哪些限制,然后再发送请求来获取数据本身。

 

6. 中止请求

为了演示这个功能,修改 fruitselector.js 这段 Node.js 脚本来引入一个2秒延迟,代码如下所示:

var http = require('http');http.createServer(function (req,res){  console.log("[200] "+req.method+" to "+req.url);    res.statusCode = 200;  res.setHeader("Content-Type","text/html");    setTimeout(function(){      var origin = req.headers["origin"];  if(origin.indexOf("localhost:63342") > -1){    res.setHeader("Access-Control-Allow-Origin",origin)  }    res.write('<html><head><title>Fruit Total</title></head><body>');  res.write('<p>');  res.write('You selectd '+req.url.substring(1));  res.write('</p></body></html>');  res.end();  },2000);    }).listen(8080);

当服务器接收到一个请求,它会先写入初始的响应标头,暂停3秒钟后再完成整个响应。下面代码展示了如何在浏览器上使用

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <title>中止请求</title></head><body><div>  <button>apples</button>  <button>bananas</button>  <button>lemons</button></div><div>  <button id="abortbutton">Abort</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){    if(e.target.id == "abortbutton"){       httpRequest.abort();    }else {      httpRequest = new = handleResponse;      httpRequest.onabort = handleAbort;      httpRequest.open("GET","http://127.0.0.1:8080/"+ e.target.innerHTML);      httpRequest.send();      document.getElementById("target").innerHTML = "Request Started";    }  }  function handleResponse(){    if(httpRequest.readyState == 4 && httpRequest.status == 200){      document.getElementById("target").innerHTML = httpRequest.responseText;    }  }  function handleAbort(){    document.getElementById("target").innerHTML = "Request Aborted";  }</script></body></html>

这里给文档添加了一个Abort(中止按钮),它通过调用

 

来源:《HTML5权威指南》(《The Definitive Guide to HTML5》)