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 行為。
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 瀏覽器是如何觸發這些事件的。
3. 處理錯誤
使用 Ajax 時必須留心兩類錯誤。它們之間的區別源於視角不同。
第一類錯誤是從 XMLHttpRequest 對象的角度看到的問題:某些因素阻止了請求發送到伺服器。例如 DNS 無法解析主機名稱,串連請求被拒絕,或者URL無效。
第二類問題是從應用程式的角度看到的問題,而非 XMLHttpRequest 對象。它們發生於請求成功發送至伺服器,伺服器接收請求、進行處理並產生響應,但該響應並不指向你期望的內容。例如,如果請求的URL 不存在,這類問題就會發生。
有三種方式可以處理這些錯誤,如下面代碼所示:
3.1 處理設定錯誤
需要處理的第一類問題是向 XMLHttpResquest 對象傳遞了錯誤的資料,比如格式不正確的 URL 。它們極其容易發生在產生基於使用者輸入的URL 時。為了類比這類問題,上面文檔中有添加一個標籤 Bad URL (錯誤的URL)的button 。按下這個按鈕會以以下形式調用 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 對象獲得何種程度的資訊取決於瀏覽器,遺憾的是大多數情況下,會得到的值為 0的 status和空白的 statusText 值。
第二個問題是URL和產生請求的具有不同的來源,在預設情況下這是不允許的。你通常只能向載入指令碼的同源URL發送Ajax請求。瀏覽器報告這個問題時可能會拋出 Error 或者觸發error事件,不同瀏覽器的處理方法不盡相同。不同瀏覽器還會在不同的時點檢查來源,這就意味著不一定總是能看到瀏覽器對同一問題反白。可以使用跨站資源規範(CORS,Cross-Origin Resource Sharing)來繞過同源限制。
3.3 處理應用程式錯誤
最後一類錯誤發生於請求成功完成(從XMLHttpRequest對象的角度看),但沒有返回你想要的資料時。為了製造這類問題,在上面樣本中添加一個說明標籤為 cucumber 的 button 。按下這個按鈕會產生類似於 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的值。而在真正的應用程式裡,需要以一種有用且有意義的方式進行恢複(比如顯示備用內容或警告使用者有問題,具體看哪種更適合應用程式)。
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日)。