Ajax:Asynchronous JavaScript + XML的簡寫。
Ajax技術(shù)的核心是XMLHttpRequest對象(XHR),XHR為向服務(wù)器發(fā)送請求和解析服務(wù)器響應(yīng)提供了流暢的接口。能夠以異步方式從服務(wù)器取得信息,再通過DOM將新數(shù)據(jù)插入到頁面中。
XMLHttpRequest對象
IE7+ Firefox Opera Chrome Safari原生支持XHR對象。
var xhr = new XMLHttpRequest();
XHR的用法
在使用XHR對象時,要調(diào)用的第一個方法是open(),它接受3個參數(shù):要發(fā)送的請求類型(get,post)、請求的URL、表示是否異步發(fā)送請求的布爾值。
這時就啟動了一個請求以備發(fā)送。
xhr.open("get", "example.php", false);
只能向同一個域中使用相同端口和協(xié)議的URL發(fā)送請求,如果URL與啟動請求的頁面有任何差別,都會引發(fā)安全錯誤。
要發(fā)送請求就要調(diào)用send()方法:
xhr.send(null);
send方法接收的參數(shù)是要作為請求主體發(fā)送的數(shù)據(jù),如果不需要數(shù)據(jù)就傳null,因為有的瀏覽器必須要傳這個數(shù)據(jù)。
如果請求是同步的,那么JS會在等到服務(wù)器響應(yīng)之后再繼續(xù)執(zhí)行。
等到響應(yīng)后,響應(yīng)的數(shù)據(jù)會自動填充XHR對象的屬性:
- responseText:響應(yīng)主體的文本
- responseXML:這個屬性中保存著包含響應(yīng)數(shù)據(jù)的XML DOM文檔
- status:響應(yīng)的HTTP狀態(tài)
- statusText:HTTP狀態(tài)的說明
var xhr = new XMLHttpRequest();
xhr.open("get", "16_.html", false);
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
在發(fā)送異步請求時JS不會等待響應(yīng),此時的XHR會有readyState屬性,表示請求響應(yīng)過程的當(dāng)前活動階段:
- 0:未初始化,尚未調(diào)用open方法
- 1:啟動,已經(jīng)調(diào)用open未調(diào)用send
- 2:發(fā)送,已經(jīng)調(diào)用send,未收到響應(yīng)
- 3:接收,已經(jīng)接收到部分響應(yīng)數(shù)據(jù)
- 4:完成,已經(jīng)接收到全部響應(yīng)數(shù)據(jù),且數(shù)據(jù)可用
readyState屬性發(fā)生改變時會觸發(fā)一次readystatechange事件,為確保瀏覽器兼容,需要在調(diào)用open前指定readystatechange事件處理程序。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
alert(xhr.readyState);
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "16_.html", true);
xhr.send(null);
在接收到響應(yīng)之前可以通過abort方法取消異步請求
xhr.abort();
在終止請求,應(yīng)該解引用XHR對象,由于內(nèi)存原因不建議重用XHR對象。
HTTP頭部信息
每個HTTP請求和響應(yīng)都會帶有相應(yīng)的頭部信息,XHR對象也提供了操作這兩種頭部信息的方法。
默認(rèn)情況下,在發(fā)送XHR請求的同時還會發(fā)送下列頭部:
- Accept
- Accept-Charset
- Accept-Encoding
- Accept-Language
- Connection
- Cookie
- Host
- Referer
- User-Agent
使用setRequestHeader()方法可以設(shè)置自定義的請求頭部。參數(shù)為頭部字段的名稱和頭部字段的值。這個要在open和send之間調(diào)用才有效。
xhr.setRequestHeader("MyHeader", "MyValue");
在使用這個方法時,建議使用自定義的頭部名,以免與瀏覽器發(fā)生沖突,有的瀏覽器可能不允許開發(fā)人員重寫默認(rèn)頭部。
想要獲得頭部的值可以使用下面這兩種方法。
var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllResponseHeaders();
GET請求
用于向服務(wù)器查詢某些信息,將參數(shù)放到后面。使用addURLParam就是保證URI被正確編碼,格式良好。
var xhr = new XMLHttpRequest();
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
var url = "example.php";
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
xhr.open("get", url, true);
xhr.send(null);
POST請求
用于向服務(wù)器發(fā)送應(yīng)該保存的數(shù)據(jù),POST請求應(yīng)該會發(fā)送很多的數(shù)據(jù)到服務(wù)器。這時send的參數(shù)就是發(fā)送的數(shù)據(jù)了。一般是數(shù)據(jù)序列化后的字符串。
var xhr = new XMLHttpRequest();
xhr.open("post", "postexample.php", true);
//模仿表單提交
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var form = document.getElementById("user-info");
xhr.send(serialize(form));
GET請求消耗的資源少,同等數(shù)據(jù)量是POST的兩倍。
XMLHttpRequest 2級
FormData
Web應(yīng)用中頻繁使用的一項功能就是表單數(shù)據(jù)的序列化。為此,2級定義了FormData對象。
append方法可以向其添加數(shù)據(jù),鍵值對形式
var data = new FormData();
data.append("name", "Nicholas");
也可以直接使用表單初始化FormData:
var data = new FormData(document.forms[0]);
xhr.send(data);
創(chuàng)建好的FormData的實例后直接傳給XHR的send方法,這時XHR會自動幫你照顧好頭部信息。
超時設(shè)定
2級中又規(guī)定了一個timeout屬性,表示請求在等待響應(yīng)多少毫秒之后就終止。設(shè)置之后,如果超時沒有接收到響應(yīng),就回觸發(fā)timeout事件,調(diào)用事件處理程序,這時xhr.readyState可能已經(jīng)為4了,但是此時請求已經(jīng)中止了,不能再訪問xhr.status,所以如果使用timeout,onreadystatechange事件處理也要小心。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
try {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
} catch (ex){
}
}
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // IE8+
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);
overrideMimeType()方法
重寫XHR響應(yīng)的MIME類型,因為響應(yīng)的MIME類型決定了XHR對象如何處理它。如果服務(wù)器返回的是XML文件,MIME卻是text/plain,那XHR對象就不能正確的設(shè)置responseXML。
要在send之前調(diào)用這個方法。
var xhr = createXHR(); xhr.open("get", "text.php", true); xhr.overrideMimeType("text/xml"); xhr.send(null);
進(jìn)度事件
Progress Events定義了與客戶端服務(wù)器通信有關(guān)的事件,這些事件最早其實只針對XHR,但目前也被其它API借鑒,有6個進(jìn)度事件:
- loadstars:在接收到響應(yīng)數(shù)據(jù)的第一個字節(jié)時觸發(fā)
- progress:在接收響應(yīng)期間不斷觸發(fā)
- error:在請求發(fā)生錯誤時觸發(fā)
- abort:在因為調(diào)用abort()方法而終止連接時觸發(fā)
- load:在接收到完整響應(yīng)數(shù)據(jù)時觸發(fā)
- loaded:在通信完成(觸發(fā)error、abort、load事件后)時觸發(fā)
支持前5個事件的有Firefox 3.5+、Safari 4+、Chrome、iOS 版Safari、Android 版 WebKit。
Opera 11+、IE8+只支持load事件。
load事件
只要瀏覽器接收到服務(wù)器的響應(yīng),不管其狀態(tài)如何都會觸發(fā)load事件,這就意味著必須檢查XHR對象的status屬性。對于這個事件有的瀏覽器實現(xiàn)了event對象,其target指向XHR對象,直接就可以使用所有對象和方法,可是有的瀏覽器并沒有實現(xiàn)。這就是說還得使用原來的全局XHR變量對象。
var xhr = new XMLHttpRequest();
xhr.onload = function(){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
progress事件
這個事件會在瀏覽器接收數(shù)據(jù)的過程中持續(xù)的觸發(fā),其target屬性為XHR對象再加額外3個屬性:
- lengthComputable:表示進(jìn)度信息是否可用
- position:表示已經(jīng)接收到的字節(jié)數(shù)
- totalSize:根據(jù)Content-Length響應(yīng)頭部確定的預(yù)期字節(jié)數(shù)
這個可以用來作為進(jìn)度指示器
var xhr = new XMLHttpRequest();
xhr.onprogress = function(event){
var divStatus = document.getElementById("highDiv");
if (event.lengthComputable) {
divStatus.innerHTML = "Received " + event.position + " of " +
event.totalSize + " bytes";
}
};
xhr.open("get", "img/paypal2.png", true);
xhr.send(null);
跨源資源共享
通過XHR實現(xiàn)Ajax通信的一個主要限制,來源于跨域安全策略。默認(rèn)情況下,XHR對象只能訪問與包含它的頁面位于同一個域中的資源。這種安全策略可以預(yù)防某些惡意行為,但實現(xiàn)合理的跨域請求也是一個很重要的需求。
CORS(Cross-Orign Resource Sharing)就是為了這樣的需求而生的。
其背后的思想就是使用自定義的HTTP頭部讓瀏覽器與服務(wù)器進(jìn)行溝通,從而決定請求或響應(yīng)應(yīng)該成功還是失敗。
比如一個簡單的使用GET或POST發(fā)送的請求,添加一個自定義頭部Origin來包含請求頁面的源信息(協(xié)議,域名和端口),以便服務(wù)器根據(jù)這個頭部信息來決定是否響應(yīng)。如果服務(wù)器認(rèn)為這個請求可以接受,那么就在Access-Control-Allow-Origin頭部中發(fā)回相同的源或*,瀏覽器會檢查這個頭部,如果不符合則駁回請求。
這里的跨域請求和響應(yīng)都不會包含cookie。
IE對CORS的實現(xiàn)
IE8中引入了XDR(XDomainRequest)類型,它與XHR相似,是用來實現(xiàn)安全可靠的跨域通信的。在安全機制方面,XDR實現(xiàn)了部分CORS規(guī)范:
- cookie不會隨請求發(fā)送,也不會隨響應(yīng)返回
- 只能設(shè)置請求頭部中的Content-Type字段
- 不能訪問響應(yīng)頭部
- 只支持GET和POST
這些變化使得CSRF跨站點請求偽造和跨站點腳本的問題得到緩解,被請求的資源可以根據(jù)它認(rèn)為合適的請求來決定是否設(shè)置Access-Control- Allow-Origin。作為請求的一部分Origin會自動設(shè)置為當(dāng)前域。XDR只能發(fā)送異步請求。請求返回后會觸發(fā)load事件。
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.onerror = function(){
alert("An error occurred.");
};
xdr.open("post", "http://www.somewhere-else.com/page/");
xdr.contentType = "application/x-www-form-urlencoded";
xdr.send("name1=value1&name2=value2");
因為無法讀取響應(yīng)頭部,也就無法獲取狀態(tài)碼。只要響應(yīng)有效就回觸發(fā)load事件,失敗就觸發(fā)error事件。
abort方法可以終止請求
XDR也有timeout屬性和ontimeout事件處理程序。
其它瀏覽器對CORS的實現(xiàn)
其它大多數(shù)瀏覽器并沒有使用新類型的對象來實現(xiàn)CORS,而是直接使用XHR原生支持。
如果要訪問其他域下的資源在XHR的open方法中傳入絕對URL即可。
這里不僅可以訪問到status和statusText屬性,還可以發(fā)起同步請求。
不過限制還是有的:
- 不能發(fā)送接受cookie
- 不能使用setRequestHeader()設(shè)置自定義頭部
- 調(diào)用getAllResponseHeaders()總會返回空字符串
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
} };
xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null);
Preflighted Reqeusts
CORS使用Preflighted Reqeusts的透明服務(wù)器驗證機制支持開發(fā)人員使用自定義頭部和GET或POST之外的方法以及不同類型的主體內(nèi)容。
在使用下列高級選項來發(fā)送請求時,就會向服務(wù)器發(fā)送一個Preflight請求。這種請求使用OPTIONS方法
- Origin:與簡單請求相同
- Access-Control-Request-Method:請求自身使用的方法
- Access-Control-Request-Headers:自定義的頭部
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
發(fā)送這個請求后,服務(wù)器可以決定是否允許這種類型的請求并發(fā)送響應(yīng)頭部與瀏覽器溝通:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Max-Age:這個Preflighted請求應(yīng)該被緩存多久
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
IE10及之前的版本不支持
帶憑據(jù)的請求
默認(rèn)情況下,跨源請求不帶憑據(jù)(cookie,HTTP認(rèn)證,SSL證明等)通過將withCredentials屬性設(shè)置為true,可以指定某個請求應(yīng)該發(fā)送憑據(jù)。如果服務(wù)器接受帶憑據(jù)的請求,會響應(yīng):Access-Control-Allow-Credentials: true。如果發(fā)送的是帶憑據(jù)的請求,而服務(wù)器中的響應(yīng)沒有包含這個頭部,那么JS獲取不到這個響應(yīng)中的信息。
跨瀏覽器的CORS
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("get", "http://www.baidu.com/");
if (request){
request.onload = function(){
alert(request.responseText);
};
request.send();
}
其它跨域技術(shù)
在CORS出現(xiàn)之前,開發(fā)人員通過各種hack方式來實現(xiàn)跨域Ajax。
圖像Ping
這個利用的是一個網(wǎng)頁可以從任何地方加載圖像,而不用擔(dān)心跨域的問題。通過向一個URL請求圖片的方式,可以向服務(wù)器單向使用GET傳送參數(shù),由于img元素的本身限制,并不能獲取服務(wù)器發(fā)來的除圖像以外的數(shù)據(jù)(響應(yīng)文本)。只能通過監(jiān)聽load和error事件知道響應(yīng)何時收到。
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";
響應(yīng)從圖片對象的src被設(shè)置時就開始了。
這個最大的用處就是跟蹤用戶點擊或廣告曝光次數(shù),網(wǎng)頁被打開了就使用img向紀(jì)錄服務(wù)器發(fā)送個規(guī)定的參數(shù),簡單可靠。
不過只能使用GET方法和無法獲得響應(yīng)文本是其局限。
JSONP
JSON with padding參數(shù)式JSON
JSONP利用動態(tài)script元素從其他域中加載資源,將服務(wù)器傳回的數(shù)據(jù)以JSON格式傳入一個回調(diào)函數(shù)中,形式就像這樣:
callback({ "name": "Nicholas" });
在實際使用時先創(chuàng)建一個用于處理數(shù)據(jù)的回調(diào)函數(shù),創(chuàng)建一個script對象,將回調(diào)函數(shù)的函數(shù)名作為其src的URL的一個參數(shù)傳遞給服務(wù)器端,服務(wù)器端根據(jù)這個URL中的參數(shù)們?nèi)〉庙憫?yīng)的數(shù)據(jù),并作為函數(shù)參數(shù)塞到回調(diào)函數(shù)中,返回一個像上面形式的對回調(diào)函數(shù)的調(diào)用。這是一個可執(zhí)行的JS,被放在了一個動態(tài)創(chuàng)建的script元素中,當(dāng)你把這個元素插入文檔,就相當(dāng)于執(zhí)行了這個有服務(wù)器數(shù)據(jù)作為參數(shù)的回調(diào)函數(shù)。
function handleResponse(response){
alert("You re at IP address " + response.ip + ", which is in " +
response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
這個就可以實現(xiàn)瀏覽器和服務(wù)器間的雙向通信了,不過有兩點問題:
- 必須確保你請求的Web服務(wù)是可靠的,否則就是你親手給惡意代碼提供了執(zhí)行的機會
- 對于確定何時接到請求或請求何時失敗并不好判斷,script元素在HTML5規(guī)范中是有error事件的,不過沒人實現(xiàn),現(xiàn)在一個比較笨的解決辦法是使用計時器手動檢測請求超時
Comet
Comet是一種服務(wù)器像瀏覽器推送數(shù)據(jù)的技術(shù)??梢宰屝畔⒔鯇崟r的被推送到頁面上。有兩種實現(xiàn)Comet的方式,長輪詢和流。
長輪詢
頁面發(fā)起一個到服務(wù)器的請求,服務(wù)器連接一直打開,知道有數(shù)據(jù)可發(fā)送,瀏覽器接收完數(shù)據(jù)后關(guān)閉該連接,發(fā)起一個新的請求。
流
HTTP流的實現(xiàn)方式則不同,在頁面的整個生命周期內(nèi)只會有一個HTTP連接。瀏覽器向服務(wù)器發(fā)送一個請求,服務(wù)器保持連接打開,周期性的向瀏覽器發(fā)送數(shù)據(jù)。一般來說,都是把新的數(shù)據(jù)加到之前輸出緩存的末尾再重新都發(fā)一遍。
通過監(jiān)聽readystatechange事件,當(dāng)readyState為3時就代表數(shù)據(jù)都接收到了。隨著不斷從服務(wù)器端接收到數(shù)據(jù),readyState會周期性的變?yōu)?。這時就可以讀取responseText中的數(shù)據(jù),并與上次接收到的做比對,獲取新的數(shù)據(jù)。
function createStreamingClient(url, progress, finished){
var xhr = new XMLHttpRequest(),
received = 0;
xhr.open("get", url, true);
xhr.onreadystatechange = function(){
var result;
if (xhr.readyState == 3){
// 數(shù) 數(shù)
result = xhr.responseText.substring(received);
received += result.length;
// progress 數(shù)
progress(result);
} else if (xhr.readyState == 4){
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}
var client = createStreamingClient("streaming.php", function(data){
alert("Received: " + data);
}, function(data){
alert("Done!"+ data);
});
Comet連接比較容易出錯,不過大家都看好這項技術(shù),為了簡化這項技術(shù)的使用成本,提高可靠性,又有兩個新的接口出現(xiàn)了。服務(wù)器發(fā)送事件(Server-Sent Events,SSE)和Web Socket。
服務(wù)器發(fā)送事件
Server-Sent Events,SSE,這是圍繞只讀Comet推出的API,用于創(chuàng)建到服務(wù)器的單向連接。服務(wù)器通過這個連接可以發(fā)送任意數(shù)量的數(shù)據(jù)。服務(wù)器響應(yīng)的MIME類型必須是text/event-stream,并且是瀏覽器中的JS能解析的格式輸出。SSE支持短輪詢,長輪詢,HTTP流。并且在斷開連接時自動確定何時重連。
支持SSE的瀏覽器有:Firefox 6+ Safari 5+ Opera 11+ Chrome iOS 4+版 Safari。
SSE API
首先新建一個EventSource對象,參數(shù)為入口點URL,這個URL要與創(chuàng)建對象的頁面同源(相同URL模式,域,及端口)。EventSource對象的readyState屬性:
- 0:正在連接服務(wù)器
- 1:打開了連接
- 2:關(guān)閉了連接。
三個事件:
- open建立連接時
- message收到新服務(wù)器事件時
- error連接錯誤時
在連接斷開時會自己重新連接。這就非常適合長輪詢,HTTP流。
想要手動斷開,有close方法。
var source = new EventSource("myevents.php");
source.onmessage = function(event){
var data = event.data;
};
source.close();
事件流
服務(wù)器事件會通過一個持久的HTTP響應(yīng)發(fā)送,這個響應(yīng)的MIME類型為text/event-stream,格式為純文本。最簡單的情況是每個數(shù)據(jù)項都有前綴data:
data: foo
data: bar
data: foo
data: bar
對應(yīng)的message事件有3個,event.data的值分別為foo,bar,foo/bar。在服務(wù)器端要注意,只有data:后面有空行時才回觸發(fā)message事件。
data: foo
id: 1
id: 2
data: foo
data: bar
id可以將事件與一個ID綁定,id的位置可以在data的前后。如果連接斷開,會向服務(wù)器發(fā)送Last-Event-ID頭部,以便服務(wù)器知道下次該發(fā)什么。
Web Socket
其目標(biāo)是在一個單獨的持久連接上提供一個全雙工的雙向通信。在Web Socket建立以后,會有一個HTTP請求發(fā)送到服務(wù)器,在取得響應(yīng)后,連接會開始使用Web Socket協(xié)議。其URL模式為ws:// , wss://。
使用Web Socket意味著更小的開銷,但是同時這個協(xié)議的問題還在不斷的被發(fā)現(xiàn),現(xiàn)在支持這個協(xié)議的有Firefox 6+ Safari 5+ Chrome iOS 4+版 Safari。
Web Sockets API
Web Socket的構(gòu)造函數(shù)只接受絕對URL。并不要求同源,因為服務(wù)器可以判斷并決定連接可以獲取什么樣的資源。
實例化后,連接馬上會創(chuàng)建。readyState:
- WebSocket.OPENING (0)
- WebSocket.OPEN (1)
- WebSocket.CLOSING (2)
- WebSocket.CLOSE (3)
關(guān)閉時使用close()
發(fā)送和接收
發(fā)送只能是純文本數(shù)據(jù),所以復(fù)雜的數(shù)據(jù)結(jié)構(gòu)要序列化。
var socket = new WebSocket("ws://www.example.com/server.php");
var message = {
time: new Date(),
text: "Hello world!",
clientId: "asdfp8734rew"
};
socket.send(JSON.stringify(message));
接受時會觸發(fā)message事件
socket.onmessage = function(event){
var data = event.data;
};
其他事件
open,error,close
安全
跨站請求偽造是Ajax遇到的最大問題,如果你可以通過Ajax的URL獲取,直接輸URL也可以。
所以我們要確認(rèn)請求發(fā)送者是否有訪問資源的權(quán)限。有下列方式可以選擇:
- SSL保護
- 每次請求附帶算法計算的驗證碼
下面的方法沒用,很容易偽造:
- 檢查URL是否可信
- 基于cookie驗證
XHR對象本身提供了一些安全機制,open 方法其實可以傳用戶名和密碼??墒窃贘S中保存這些,用調(diào)試器一下就看到明文了。