??2005 年,Jesse James Garrett 發(fā)表了一篇在線文章,題為“Ajax: A new Approach to Web Applications” 。他在這篇文章里介紹了一種技術(shù),用他的話說,就叫 Ajax,是對 Asynchronous JavaScript + XML 的簡寫。
(http://www.adaptivepath.com/ideas/essays/archives/000385.php)
??這一技術(shù)能夠像服務(wù)器請求額外的數(shù)據(jù)而無需卸載頁面,會帶來更好的用戶體驗。Garrett 還解釋了怎樣使用這一技術(shù)改變自從 Web 誕生以來就一直沿用的“單擊”,“等待”的交互模式。
??Ajax 技術(shù)的核心是 XMLHttpRequest 對象(簡稱 XHR),這是由微軟首先引入的一個特性,其他瀏覽器提供商后來都提供了相同的實現(xiàn)。
??在 XHR 出現(xiàn)之前,Ajax 式的通信必須借助一些 hack 手段來實現(xiàn),大多數(shù)是使用隱藏的框架或內(nèi)嵌框架。
??XHR 為向服務(wù)器發(fā)送請求和解析服務(wù)器響應(yīng)提供了流暢的接口。能夠以異步方式從服務(wù)器取得更多信息,意味著用戶單擊后,可以不必刷新頁面也能取得新數(shù)據(jù)。
??也就是說,可以使用 XHR 對象取得新數(shù)據(jù),然后再通過 DOM 將新數(shù)據(jù)插入到頁面中。另外,雖然名字中包含 XML 的成分,但 Ajax 通信與數(shù)據(jù)格式無關(guān);這種技術(shù)就是無須刷新頁面即可從服務(wù)器取得數(shù)據(jù),但不一定是 XML 數(shù)據(jù)。
??實際上,Garrett 提到的這種技術(shù)已經(jīng)存在很長時間了。在 Garrett 撰寫那篇文章之前,人們通常將這種技術(shù)叫做遠程腳本(remote scripting),而且早在 1998 年就有人采用不同的手段實現(xiàn)了這種瀏覽器與服務(wù)器的通信。再往前推,JavaScript 需要通過 Java applet 或 Flash 電影等中間層向服務(wù)器發(fā)送請求。
??而 XHR 則將瀏覽器原生的通信能力提供給了開發(fā)人員,簡化了實現(xiàn)同樣操作的任務(wù)。
??在重命名為 Ajax 之后,大約是 2005 年底 2006 年初,這種瀏覽器與服務(wù)器的通信技術(shù)可謂紅極一時。人們對 JavaScript 和 Web 的全新認識,催生了很多使用原有特性的新技術(shù)和新模式。
??就目前來說,熟練使用 XHR 對象已經(jīng)成為所有 Web 開發(fā)人員必須掌握的一種技能。
1、XMLHttpRequest 對象
??IE5 是第一款引入 XHR 對象的瀏覽器。在 IE5 中,XHR 對象是通過 MSXML 庫中的一個 ActiveX 對象實現(xiàn)的。因此,在 IE 中可能會遇到三種不同版本的 XHR 對象,即 MSXML2.XMLHttp、MSXML2.XMLHttp.3.0 和 MXSML2.XMLHttp.6.0。
??IE7+、Firefox、Opera、Chrome 和 Safari 都支持原生的 XHR 對象,在這些瀏覽器中創(chuàng)建 XHR 對象要像下面這樣使用 XMLHttpRequest 構(gòu)造函數(shù)。
var xhr = new XMLHttpRequest();
??如果你必須還要支持 IE 的早期版本,那么則可以使用下面這個函數(shù)。
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"],
i, len;
for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
// 跳過
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
??這個函數(shù)中新增的代碼首先檢測原生 XHR 對象是否存在,如果存在則返回它的新實例。如果原生對象不存在,則檢測 ActiveX 對象。如果這兩種對象都不存在,就拋出一個錯誤。
??然后,就可以使用下面的代碼在所有瀏覽器中創(chuàng)建 XHR 對象了。
var xhr = createXHR();
??由于其他瀏覽器中對 XHR 的實現(xiàn)與 IE 最早的實現(xiàn)是兼容的,因此就可以在所有瀏覽器中都以相同方式使用上面創(chuàng)建的 xhr 對象。
1.1、XHR 的用法
??在使用 XHR 對象時,要調(diào)用的第一個方法是 open(),它接受 3 個參數(shù):要發(fā)送的請求的類型("get"、"post"等)、請求的 URL 和表示是否異步發(fā)送請求的布爾值。下面就是調(diào)用這個方法的例子。
xhr.open("get", "example.php", false);
??這行代碼會啟動一個針對 example.php 的 GET 請求。有關(guān)這行代碼,需要說明兩點:一是 URL 相對于執(zhí)行代碼的當(dāng)前頁面(當(dāng)然也可以使用絕對路徑);二是調(diào)用 open() 方法并不會真正發(fā)送請求,而只是啟動一個請求以備發(fā)送。
??只能向同一個域中使用相同端口和協(xié)議的 URL 發(fā)送請求。如果 URL 與啟動請求的頁面有任何差別,都會引發(fā)安全錯誤。
??要發(fā)送特定的請求,必須像下面這樣調(diào)用 send() 方法:
xhr.open("get", "example.txt", false);
xhr.send(null);
??這里的 send() 方法接收一個參數(shù),即要作為請求主體發(fā)送的數(shù)據(jù)。如果不需要通過請求主體發(fā)送數(shù)據(jù),則必須傳入 null,因為這個參數(shù)對有些瀏覽器來說是必需的。
??調(diào)用 send() 之后,請求就會被分派到服務(wù)器。由于這次請求是同步的,JavaScript 代碼會等到服務(wù)器響應(yīng)之后再繼續(xù)執(zhí)行。在收到響應(yīng)后,響應(yīng)的數(shù)據(jù)會自動填充 XHR 對象的屬性,相關(guān)的屬性簡介如下。
- responseText:作為響應(yīng)主體被返回的文本。
- responseXML:如果響應(yīng)的內(nèi)容類型是"text/xml"或"application/xml",這個屬性中將保存包含著響應(yīng)數(shù)據(jù)的 XML DOM 文檔。
- status:響應(yīng)的 HTTP 狀態(tài)。
- statusText:HTTP 狀態(tài)的說明。
??在接收到響應(yīng)后,第一步是檢查 status 屬性,以確定響應(yīng)已經(jīng)成功返回。一般來說,可以將 HTTP 狀態(tài)代碼為 200 作為成功的標志。此時,responseText 屬性的內(nèi)容已經(jīng)就緒,而且在內(nèi)容類型正確的情況下,responseXML 也應(yīng)該能夠訪問了。
??此外,狀態(tài)代碼為 304 表示請求的資源并沒有被修改,可以直接使用瀏覽器中緩存的版本;當(dāng)然,也意味著響應(yīng)是有效的。為確保接收到適當(dāng)?shù)捻憫?yīng),應(yīng)該像下面這樣檢查上述這兩種狀態(tài)代碼:
xhr.open("get", "example.txt", false);
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
??根據(jù)返回的狀態(tài)代碼,這個例子可能會顯示由服務(wù)器返回的內(nèi)容,也可能會顯示一條錯誤消息。
??我們建議讀者要通過檢測 status 來決定下一步的操作,不要依賴 statusText,因為后者在跨瀏覽器使用時不太可靠。
??另外,無論內(nèi)容類型是什么,響應(yīng)主體的內(nèi)容都會保存到 responseText 屬性中;而對于非 XML 數(shù)據(jù)而言,responseXML 屬性的值將為 null。
??有的瀏覽器會錯誤地報告 204 狀態(tài)代碼。IE 中 XHR 的 ActiveX 版本會將 204 設(shè)置為 1223,而 IE 中原生的 XHR 則會將 204 規(guī)范化為 200。Opera 會在取得 204 時報告 status 的值為 0。
??像前面這樣發(fā)送同步請求當(dāng)然沒有問題,但多數(shù)情況下,我們還是要發(fā)送異步請求,才能讓 JavaScript 繼續(xù)執(zhí)行而不必等待響應(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ù),而且已經(jīng)可以在客戶端使用了。
??只要 readyState 屬性的值由一個值變成另一個值,都會觸發(fā)一次 readystatechange 事件??梢岳眠@個事件來檢測每次狀態(tài)變化后 readyState 的值。
??通常,我們只對 readyState 值為 4 的階段感興趣,因為這時所有數(shù)據(jù)都已經(jīng)就緒。不過,必須在調(diào)用 open() 之前指定 onreadystatechange 事件處理程序才能確??鐬g覽器兼容性。下面來看一個例子。
var xhr = createXHR();
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", "example.txt", true);
xhr.send(null);
??以上代碼利用 DOM 0 級方法為 XHR 對象添加了事件處理程序,原因是并非所有瀏覽器都支持 DOM 2 級方法。
??與其他事件處理程序不同,這里沒有向 onreadystatechange 事件處理程序中傳遞 event 對象;必須通過 XHR 對象本身來確定下一步該怎么做。
??這個例子在 onreadystatechange 事件處理程序中使用了 xhr 對象,沒有使用 this 對象,原因是 onreadystatechange 事件處理程序的作用域問題。如果使用 this 對象,在有的瀏覽器中會導(dǎo)致函數(shù)執(zhí)行失敗,或者導(dǎo)致錯誤發(fā)生。因此,使用實際的 XHR 對象實例變量是較為可靠的一種方式。
??另外,在接收到響應(yīng)之前還可以調(diào)用 abort() 方法來取消異步請求,如下所示:
xhr.abort();
??調(diào)用這個方法后,XHR 對象會停止觸發(fā)事件,而且也不再允許訪問任何與響應(yīng)有關(guān)的對象屬性。在終止請求之后,還應(yīng)該對 XHR 對象進行解引用操作。由于內(nèi)存原因,不建議重用 XHR 對象。
1.2、HTTP 頭部信息
??每個 HTTP 請求和響應(yīng)都會帶有相應(yīng)的頭部信息,其中有的對開發(fā)人員有用,有的也沒有什么用。
??XHR 對象也提供了操作這兩種頭部(即請求頭部和響應(yīng)頭部)信息的方法。
??默認情況下,在發(fā)送 XHR 請求的同時,還會發(fā)送下列頭部信息。
- Accept:瀏覽器能夠處理的內(nèi)容類型。
- Accept-Charset:瀏覽器能夠顯示的字符集。
- Accept-Encoding:瀏覽器能夠處理的壓縮編碼。
- Accept-Language:瀏覽器當(dāng)前設(shè)置的語言。
- Connection:瀏覽器與服務(wù)器之間連接的類型。
- Cookie:當(dāng)前頁面設(shè)置的任何 Cookie。
- Host:發(fā)出請求的頁面所在的域 。
- Referer:發(fā)出請求的頁面的 URI。注意,HTTP 規(guī)范將這個頭部字段拼寫錯了,而為保證與規(guī)范一致,也只能將錯就錯了。(這個英文單詞的正確拼法應(yīng)該是 referrer。)
- User-Agent:瀏覽器的用戶代理字符串。
??雖然不同瀏覽器實際發(fā)送的頭部信息會有所不同,但以上列出的基本上是所有瀏覽器都會發(fā)送的。
??使用 setRequestHeader() 方法可以設(shè)置自定義的請求頭部信息。這個方法接受兩個參數(shù):頭部字段的名稱和頭部字段的值。
??要成功發(fā)送請求頭部信息,必須在調(diào)用 open() 方法之后且調(diào)用 send() 方法之前調(diào)用 setRequestHeader(),如下面的例子所示。
var xhr = createXHR();
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", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
??服務(wù)器在接收到這種自定義的頭部信息之后,可以執(zhí)行相應(yīng)的后續(xù)操作。我們建議讀者使用自定義的頭部字段名稱,不要使用瀏覽器正常發(fā)送的字段名稱,否則有可能會影響服務(wù)器的響應(yīng)。有的瀏覽器允許開發(fā)人員重寫默認的頭部信息,但有的覽器則不允許這樣做。
??調(diào)用 XHR 對象的 getResponseHeader() 方法并傳入頭部字段名稱,可以取得相應(yīng)的響應(yīng)頭部信息。而調(diào)用 getAllResponseHeaders() 方法則可以取得一個包含所有頭部信息的長字符串。來看下面的例子。
var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllResponseHeaders();
??在服務(wù)器端,也可以利用頭部信息向瀏覽器發(fā)送額外的、結(jié)構(gòu)化的數(shù)據(jù)。在沒有自定義信息的情況下,getAllResponseHeaders() 方法通常會返回如下所示的多行文本內(nèi)容:
Date: Sun, 14 Nov 2004 18:04:03 GMT
Server: Apache/1.3.29 (Unix)
Vary: Accept
X-Powered-By: PHP/4.3.8
Connection: close
Content-Type: text/html; charset=iso-8859-1
??這種格式化的輸出可以方便我們檢查響應(yīng)中所有頭部字段的名稱,而不必一個一個地檢查某個字段是否存在。
1.3、GET 請求
??GET 是最常見的請求類型,最常用于向服務(wù)器查詢某些信息。必要時,可以將查詢字符串參數(shù)追加到 URL 的末尾,以便將信息發(fā)送給服務(wù)器。
??對 XHR 而言,位于傳入 open() 方法的 URL 末尾的查詢字符串必須經(jīng)過正確的編碼才行。
??使用 GET 請求經(jīng)常會發(fā)生的一個錯誤,就是查詢字符串的格式有問題。查詢字符串中每個參數(shù)的名稱和值都必須使用 encodeURIComponent() 進行編碼,然后才能放到 URL 的末尾;而且所有名-值對兒都必須由和號(&)分隔,如下面的例子所示。
xhr.open("get", "example.php?name1=value1&name2=value2", true);
??下面這個函數(shù)可以輔助向現(xiàn)有 URL 的末尾添加查詢字符串參數(shù):
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
??這個 addURLParam() 函數(shù)接受三個參數(shù):要添加參數(shù)的 URL、參數(shù)的名稱和參數(shù)的值。
??這個函數(shù)首先檢查 URL 是否包含問號(以確定是否已經(jīng)有參數(shù)存在)。如果沒有,就添加一個問號;否則,就添加一個和號。然后,將參數(shù)名稱和值進行編碼,再添加到 URL 的末尾。最后返回添加參數(shù)之后的 URL。
??下面是使用這個函數(shù)來構(gòu)建請求 URL 的示例。
var url = "example.php";
// 添加參數(shù)
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
//初始化請求
xhr.open("get", url, false);
??在這里使用 addURLParam() 函數(shù)可以確保查詢字符串的格式良好,并可靠地用于 XHR 對象。
1.4、POST 請求
??使用頻率僅次于 GET 的是 POST 請求,通常用于向服務(wù)器發(fā)送應(yīng)該被保存的數(shù)據(jù)。
??POST 請求應(yīng)該把數(shù)據(jù)作為請求的主體提交,而 GET 請求傳統(tǒng)上不是這樣。
??POST 請求的主體可以包含非常多的數(shù)據(jù),而且格式不限。
??在 open() 方法第一個參數(shù)的位置傳入"post",就可以初始化一個 POST 請求,如下面的例子所示。
xhr.open("post", "example.php", true);
??發(fā)送 POST 請求的第二步就是向 send() 方法中傳入某些數(shù)據(jù)。由于 XHR 最初的設(shè)計主要是為了處理 XML,因此可以在此傳入 XML DOM 文檔,傳入的文檔經(jīng)序列化之后將作為請求主體被提交到服務(wù)器。
??當(dāng)然,也可以在此傳入任何想發(fā)送到服務(wù)器的字符串。
??默認情況下,服務(wù)器對 POST 請求和提交 Web 表單的請求并不會一視同仁。因此,服務(wù)器端必須有程序來讀取發(fā)送過來的原始數(shù)據(jù),并從中解析出有用的部分。
??不過,我們可以使用 XHR 來模仿表單提交:首先將 Content-Type 頭部信息設(shè)置為 application/x-www-form-urlencoded,也就是表單提交時的內(nèi)容類型,其次是以適當(dāng)?shù)母袷絼?chuàng)建一個字符串。POST 數(shù)據(jù)的格式與查詢字符串格式相同。如果需要將頁面中表單的數(shù)據(jù)進行序列化,然后再通過 XHR 發(fā)送到服務(wù)器,那么就可以使用 serialize() 函數(shù)來創(chuàng)建這個字符串:
function serialize(form){
var parts = [],
field = null,
i,
len,
j,
optLen,
option,
optValue;
for (i=0, len=form.elements.length; i < len; i++){
field = form.elements[i];
switch(field.type){
case "select-one":
case "select-multiple":
if (field.name.length){
for (j=0, optLen = field.options.length; j < optLen; j++){
option = field.options[j];
if (option.selected){
optValue = "";
if (option.hasAttribute){
optValue = (option.hasAttribute("value") ?
option.value : option.text);
} else {
optValue = (option.attributes["value"].specified ?
option.value : option.text);
}
parts.push(encodeURIComponent(field.name) + "=" +
encodeURIComponent(optValue));
}
}
}
break;
case undefined: //字段集
case "file": //文件輸入
case "submit": //提交按鈕
case "reset": //重置按鈕
case "button": //自定義按鈕
break;
case "radio": //單選按鈕
case "checkbox": //復(fù)選框
if (!field.checked){
break;
}
/* 執(zhí)行默認操作 */
default:
//不包含沒有名字的表單字段
if (field.name.length){
parts.push(encodeURIComponent(field.name) + "=" +
encodeURIComponent(field.value));
}
}
}
return parts.join("&");
}
function submitData(){
var xhr = createXHR();
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("post", "postexample.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var form = document.getElementById("user-info");
xhr.send(serialize(form));
}
??這個函數(shù)可以將 ID 為"user-info"的表單中的數(shù)據(jù)序列化之后發(fā)送給服務(wù)器。而下面的示例 PHP 文件 postexample.php 就可以通過$_POST 取得提交的數(shù)據(jù)了:
<?php
header("Content-Type: text/plain");
echo <<<EOF
Name: {$_POST[‘user-name’]}
Email: {$_POST[‘user-email’]}
EOF;
?>
??如果不設(shè)置 Content-Type 頭部信息,那么發(fā)送給服務(wù)器的數(shù)據(jù)就不會出現(xiàn)在$_POST 超級全局變量中。這時候,要訪問同樣的數(shù)據(jù),就必須借助$HTTP_RAW_POST_DATA。
??與 GET 請求相比,POST 請求消耗的資源會更多一些。從性能角度來看,以發(fā)送相同的數(shù)據(jù)計,GET 請求的速度最多可達到 POST 請求的兩倍。
2、XMLHTtpRequest 2 級
??鑒于 XHR 已經(jīng)得到廣泛接受,成為了事實標準,W3C 也著手制定相應(yīng)的標準以規(guī)范其行為。
XMLHttpRequest 1 級只是把已有的 XHR 對象的實現(xiàn)細節(jié)描述了出來。而 XMLHttpRequest 2 級則進一步發(fā)展了 XHR。
??并非所有瀏覽器都完整地實現(xiàn)了 XMLHttpRequest 2 級規(guī)范,但所有瀏覽器都實現(xiàn)了它規(guī)定的部分內(nèi)容。
2.1、FormData
??現(xiàn)代 Web 應(yīng)用中頻繁使用的一項功能就是表單數(shù)據(jù)的序列化,XMLHttpRequest 2 級為此定義了 FormData 類型。
??FormData 為序列化表單以及創(chuàng)建與表單格式相同的數(shù)據(jù)(用于通過 XHR 傳輸)提供了便利。下面的代碼創(chuàng)建了一個 FormData 對象,并向其中添加了一些數(shù)據(jù)。
var data = new FormData();
data.append("name", "Nicholas");
??這個 append() 方法接收兩個參數(shù):鍵和值,分別對應(yīng)表單字段的名字和字段中包含的值??梢韵襁@樣添加任意多個鍵值對兒。
??而通過向 FormData 構(gòu)造函數(shù)中傳入表單元素,也可以用表單元素的數(shù)據(jù)預(yù)先向其中填入鍵值對兒:
var data = new FormData(document.forms[0]);
??創(chuàng)建了 FormData 的實例后,可以將它直接傳給 XHR 的 send() 方法,如下所示:
var xhr = createXHR();
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("post","postexample.php", true);
var form = document.getElementById("user-info");
xhr.send(new FormData(form));
??使用 FormData 的方便之處體現(xiàn)在不必明確地在 XHR 對象上設(shè)置請求頭部。XHR 對象能夠識別傳入的數(shù)據(jù)類型是 FormData 的實例,并配置適當(dāng)?shù)念^部信息。
??支持 FormData 的瀏覽器有 Firefox 4+、Safari 5+、Chrome 和 Android 3+版 WebKit。
2.2、超時設(shè)定
??IE8 為 XHR 對象添加了一個 timeout 屬性,表示請求在等待響應(yīng)多少毫秒之后就終止。在給 timeout 設(shè)置一個數(shù)值后,如果在規(guī)定的時間內(nèi)瀏覽器還沒有接收到響應(yīng),那么就會觸發(fā) timeout 事件,進而會調(diào)用 ontimeout 事件處理程序。這項功能后來也被收入了 XMLHttpRequest 2 級規(guī)范中。來看下面的例子。
var xhr = createXHR();
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){
// 假設(shè)由 ontimeout 事件處理程序處理
}
}
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // 將超時設(shè)置為 1 秒鐘(僅適用于 IE8+)
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);
??這個例子示范了如何使用 timeout 屬性。將這個屬性設(shè)置為 1000 毫秒,意味著如果請求在 1 秒鐘內(nèi)還沒有返回,就會自動終止。請求終止時,會調(diào)用 ontimeout 事件處理程序。
??但此時 readyState 可能已經(jīng)改變?yōu)?4 了,這意味著會調(diào)用 onreadystatechange 事件處理程序。
??可是,如果在超時終止請求之后再訪問 status 屬性,就會導(dǎo)致錯誤。為避免瀏覽器報告錯誤,可以將檢查 status 屬性的語句封裝在一個 try-catch 語句當(dāng)中。
2.3、overrideMimeType() 方法
??Firefox 最早引入了 overrideMimeType() 方法,用于重寫 XHR 響應(yīng)的 MIME 類型。這個方法后來也被納入了 XMLHttpRequest 2 級規(guī)范。
??因為返回響應(yīng)的 MIME 類型決定了 XHR 對象如何處理它,所以提供一種方法能夠重寫服務(wù)器返回的 MIME 類型是很有用的。比如,服務(wù)器返回的 MIME 類型是 text/plain,但數(shù)據(jù)中實際包含的是 XML。根據(jù) MIME 類型,即使數(shù)據(jù)是 XML,responseXML 屬性中仍然是 null。通過調(diào)用 overrideMimeType() 方法,可以保證把響應(yīng)當(dāng)作 XML 而非純文本來處理。
var xhr = createXHR();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
??這個例子強迫 XHR 對象將響應(yīng)當(dāng)作 XML 而非純文本來處理。調(diào)用 overrideMimeType() 必須在 send()方法之前,才能保證重寫響應(yīng)的 MIME 類型。
??支持 overrideMimeType() 方法的瀏覽器有 Firefox、Safari 4+、Opera 10.5 和 Chrome。
3、進度事件
??Progress Events 規(guī)范是 W3C 的一個工作草案,定義了與客戶端服務(wù)器通信有關(guān)的事件。這些事件最早其實只針對 XHR 操作,但目前也被其他 API 借鑒。有以下 6 個進度事件。
- loadstart:在接收到響應(yīng)數(shù)據(jù)的第一個字節(jié)時觸發(fā)。
- progress:在接收響應(yīng)期間持續(xù)不斷地觸發(fā)。
- error:在請求發(fā)生錯誤時觸發(fā)。
- abort:在因為調(diào)用 abort() 方法而終止連接時觸發(fā)。
- load:在接收到完整的響應(yīng)數(shù)據(jù)時觸發(fā)。
- loadend:在通信完成或者觸發(fā) error、abort 或 load 事件后觸發(fā)。
??每個請求都從觸發(fā) loadstart 事件開始,接下來是一或多個 progress 事件,然后觸發(fā) error、abort 或 load 事件中的一個,最后以觸發(fā) loadend 事件結(jié)束。
??支持前 5 個事件的瀏覽器有 Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 版 WebKit。Opera(從第 11 版開始)、IE 8+只支持 load 事件。目前還沒有瀏覽器支持 loadend 事件。這些事件大都很直觀,但其中兩個事件有一些細節(jié)需要注意。
3.1、load 事件
??Firefox 在實現(xiàn) XHR 對象的某個版本時,曾致力于簡化異步交互模型。最終,F(xiàn)irefox 實現(xiàn)中引入了 load 事件,用以替代 readystatechange 事件。響應(yīng)接收完畢后將觸發(fā) load 事件,因此也就沒有必要去檢查 readyState 屬性了。
??而 onload 事件處理程序會接收到一個 event 對象,其 target 屬性就指向 XHR 對象實例,因而可以訪問到 XHR 對象的所有方法和屬性。
??然而,并非所有瀏覽器都為這個事件實現(xiàn)了適當(dāng)?shù)氖录ο?。結(jié)果,開發(fā)人員還是要像下面這樣被迫使用 XHR 對象變量。
var xhr = createXHR();
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);
??只要瀏覽器接收到服務(wù)器的響應(yīng),不管其狀態(tài)如何,都會觸發(fā) load 事件。而這意味著你必須要檢查 status 屬性,才能確定數(shù)據(jù)是否真的已經(jīng)可用了。Firefox、Opera、Chrome 和 Safari 都支持 load 事件。
3.2、progress 事件
??Mozilla 對 XHR 的另一個革新是添加了 progress 事件,這個事件會在瀏覽器接收新數(shù)據(jù)期間周期性地觸發(fā)。
??而 onprogress 事件處理程序會接收到一個 event 對象,其 target 屬性是 XHR 對象,但包含著三個額外的屬性:lengthComputable、position 和 totalSize。
??其中,lengthComputable 是一個表示進度信息是否可用的布爾值,position 表示已經(jīng)接收的字節(jié)數(shù),totalSize 表示根據(jù) Content-Length 響應(yīng)頭部確定的預(yù)期字節(jié)數(shù)。有了這些信息,我們就可以為用戶創(chuàng)建一個進度指示器了。下面展示了為用戶創(chuàng)建進度指示器的一個示例。
var xhr = createXHR();
xhr.onload = function(event){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.onprogress = function(event){
var divStatus = document.getElementById("status");
if (event.lengthComputable){
divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize +" bytes";
}
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
??為確保正常執(zhí)行,必須在調(diào)用 open() 方法之前添加 onprogress 事件處理程序。在前面的例子中,每次觸發(fā) progress 事件,都會以新的狀態(tài)信息更新 HTML 元素的內(nèi)容。如果響應(yīng)頭部中包含 Content-Length 字段,那么也可以利用此信息來計算從響應(yīng)中已經(jīng)接收到的數(shù)據(jù)的百分比。
4、跨源資源共享
??通過 XHR 實現(xiàn) Ajax 通信的一個主要限制,來源于跨域安全策略。默認情況下,XHR 對象只能訪問與包含它的頁面位于同一個域中的資源。這種安全策略可以預(yù)防某些惡意行為。
??但是,實現(xiàn)合理的跨域請求對開發(fā)某些瀏覽器應(yīng)用程序也是至關(guān)重要的。
??CORS(Cross-Origin Resource Sharing,跨源資源共享)是 W3C 的一個工作草案,定義了在必須訪問跨源資源時,瀏覽器與服務(wù)器應(yīng)該如何溝通。
??CORS 背后的基本思想,就是使用自定義的 HTTP 頭部讓瀏覽器與服務(wù)器進行溝通,從而決定請求或響應(yīng)是應(yīng)該成功,還是應(yīng)該失敗。
??比如一個簡單的使用 GET 或 POST 發(fā)送的請求,它沒有自定義的頭部,而主體內(nèi)容是 text/plain。在發(fā)送該請求時,需要給它附加一個額外的 Origin 頭部,其中包含請求頁面的源信息(協(xié)議、域名和端口),以便服務(wù)器根據(jù)這個頭部信息來決定是否給予響應(yīng)。下面是 Origin 頭部的一個示例:
Origin: http://www.nczonline.net
??如果服務(wù)器認為這個請求可以接受,就在 Access-Control-Allow-Origin 頭部中回發(fā)相同的源信息(如果是公共資源,可以回發(fā)"*")。例如:
Access-Control-Allow-Origin: http://www.nczonline.net
??如果沒有這個頭部,或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。注意,請求和響應(yīng)都不包含 cookie 信息。
4.1、IE 對 CORS 的實現(xiàn)
??微軟在 IE8 中引入了 XDR(XDomainRequest)類型。這個對象與 XHR 類似,但能實現(xiàn)安全可靠的跨域通信。
??XDR 對象的安全機制部分實現(xiàn)了 W3C 的 CORS 規(guī)范。以下是 XDR 與 XHR 的一些不同之處。
- cookie 不會隨請求發(fā)送,也不會隨響應(yīng)返回。
- 只能設(shè)置請求頭部信息中的 Content-Type 字段。
- 不能訪問響應(yīng)頭部信息。
- 只支持 GET 和 POST 請求。
??這些變化使 CSRF(Cross-Site Request Forgery,跨站點請求偽造)和 XSS(Cross-Site Scripting,跨站點腳本)的問題得到了緩解。
??被請求的資源可以根據(jù)它認為合適的任意數(shù)據(jù)(用戶代理、來源頁面等)來決定是否設(shè)置 Access-Control- Allow-Origin 頭部。作為請求的一部分,Origin 頭部的值表示請求的來源域,以便遠程資源明確地識別 XDR 請求。
??XDR 對象的使用方法與 XHR 對象非常相似。也是創(chuàng)建一個 XDomainRequest 的實例,調(diào)用 open() 方法,再調(diào)用 send() 方法。
??但與 XHR 對象的 open() 方法不同,XDR 對象的 open() 方法只接收兩個參數(shù):請求的類型和 URL。
??所有 XDR 請求都是異步執(zhí)行的,不能用它來創(chuàng)建同步請求。請求返回之后,會觸發(fā) load 事件,響應(yīng)的數(shù)據(jù)也會保存在 responseText 屬性中,如下所示。
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);
??在接收到響應(yīng)后,你只能訪問響應(yīng)的原始文本;沒有辦法確定響應(yīng)的狀態(tài)代碼。而且,只要響應(yīng)有效就會觸發(fā) load 事件,如果失?。ò憫?yīng)中缺少 Access-Control-Allow-Origin 頭部)就會觸發(fā) error 事件。
??遺憾的是,除了錯誤本身之外,沒有其他信息可用,因此唯一能夠確定的就只有請求未成功了。要檢測錯誤,可以像下面這樣指定一個 onerror 事件處理程序。
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.onerror = function(){
alert("An error occurred.");
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);
??鑒于導(dǎo)致 XDR 請求失敗的因素很多,因此建議你不要忘記通過 onerror 事件處理程序來捕獲該事件;否則,即使請求失敗也不會有任何提示。
??在請求返回前調(diào)用 abort() 方法可以終止請求:
xdr.abort(); // 終止請求
??與 XHR 一樣,XDR 對象也支持 timeout 屬性以及 ontimeout 事件處理程序。下面是一個例子。
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.onerror = function(){
alert("An error occurred.");
};
xdr.timeout = 1000;
xdr.ontimeout = function(){
alert("Request took too long.");
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);
??上述例子會在運行 1 秒鐘后超時,并隨即調(diào)用 ontimeout 事件處理程序。
??為支持 POST 請求,XDR 對象提供了 contentType 屬性,用來表示發(fā)送數(shù)據(jù)的格式,如下面的例子所示。
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");
??這個屬性是通過 XDR 對象影響頭部信息的唯一方式。
4.2、其他瀏覽器對 CORS 的實現(xiàn)
??Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 平臺中的 WebKit 都通過 XMLHttpRequest 對象實現(xiàn)了對 CORS 的原生支持。在嘗試打開不同來源的資源時,無需額外編寫代碼就可以觸發(fā)這個行為。
??要請求位于另一個域中的資源,使用標準的 XHR 對象并在 open() 方法中傳入絕對 URL 即可,例如:
var xhr = createXHR();
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);
??與 IE 中的 XDR 對象不同,通過跨域 XHR 對象可以訪問 status 和 statusText 屬性,而且還支持同步請求。
??跨域 XHR 對象也有一些限制,但為了安全這些限制是必需的。以下就是這些限制。
- 不能使用 setRequestHeader()設(shè)置自定義頭部。
- 不能發(fā)送和接收 cookie。
- 調(diào)用 getAllResponseHeaders() 方法總會返回空字符串。
??由于無論同源請求還是跨源請求都使用相同的接口,因此對于本地資源,最好使用相對 URL,在訪問遠程資源時再使用絕對 URL。這樣做能消除歧義,避免出現(xiàn)限制訪問頭部或本地 cookie 信息等問題。
4.3、Preflighted Requests
??CORS 通過一種叫做 Preflighted Requests 的透明服務(wù)器驗證機制支持開發(fā)人員使用自定義的頭部、GET 或 POST 之外的方法,以及不同類型的主體內(nèi)容。在使用下列高級選項來發(fā)送請求時,就會向服務(wù)器發(fā)送一個 Preflight 請求。這種請求使用 OPTIONS 方法,發(fā)送下列頭部。
- Origin:與簡單的請求相同。
- Access-Control-Request-Method:請求自身使用的方法。
- Access-Control-Request-Headers:(可選)自定義的頭部信息,多個頭部以逗號分隔。
??以下是一個帶有自定義頭部 NCZ 的使用 POST 方法發(fā)送的請求。
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
??發(fā)送這個請求后,服務(wù)器可以決定是否允許這種類型的請求。服務(wù)器通過在響應(yīng)中發(fā)送如下頭部與瀏覽器進行溝通。
- Access-Control-Allow-Origin:與簡單的請求相同。
- Access-Control-Allow-Methods:允許的方法,多個方法以逗號分隔。
- Access-Control-Allow-Headers:允許的頭部,多個頭部以逗號分隔。
- Access-Control-Max-Age:應(yīng)該將這個 Preflight 請求緩存多長時間(以秒表示)。
??例如:
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
??Preflight 請求結(jié)束后,結(jié)果將按照響應(yīng)中指定的時間緩存起來。而為此付出的代價只是第一次發(fā)送這種請求時會多一次 HTTP 請求。
??支持 Preflight 請求的瀏覽器包括 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不支持。
4.4、帶憑據(jù)的請求
??默認情況下,跨源請求不提供憑據(jù)(cookie、HTTP 認證及客戶端 SSL 證明 等 )。 通 過 將 withCredentials 屬性設(shè)置為 true,可以指定某個請求應(yīng)該發(fā)送憑據(jù)。如果服務(wù)器接受帶憑據(jù)的請求,會用下面的 HTTP 頭部來響應(yīng)。
Access-Control-Allow-Credentials: true
??如果發(fā)送的是帶憑據(jù)的請求,但服務(wù)器的響應(yīng)中沒有包含這個頭部,那么瀏覽器就不會把響應(yīng)交給 JavaScript(于是,responseText 中將是空字符串,status 的值為 0,而且會調(diào)用 onerror() 事件處理程序)。
??另外,服務(wù)器還可以在 Preflight 響應(yīng)中發(fā)送這個 HTTP 頭部,表示允許源發(fā)送帶憑據(jù)的請求。
??支持 withCredentials 屬性的瀏覽器有 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不支持。
4.5、跨瀏覽器的 CROS
??即使瀏覽器對 CORS 的支持程度并不都一樣,但所有瀏覽器都支持簡單的(非 Preflight 和不帶憑據(jù)的)請求,因此有必要實現(xiàn)一個跨瀏覽器的方案。
??檢測 XHR 是否支持 CORS 的最簡單方式,就是檢查是否存在 withCredentials 屬性。再結(jié)合檢測 XDomainRequest 對象是否存在,就可以兼顧所有瀏覽器了。
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
vxhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("get", "http://www.somewhere-else.com/page/");
if (request){
request.onload = function(){
// 對 request.responseText 進行處理
};
request.send();
}
??Firefox、Safari 和 Chrome 中的 XMLHttpRequest 對象與 IE 中的 XDomainRequest 對象類似,都提供了夠用的接口,因此以上模式還是相當(dāng)有用的。這兩個對象共同的屬性/方法如下。
- abort():用于停止正在進行的請求。
- onerror:用于替代 onreadystatechange 檢測錯誤。
- onload:用于替代 onreadystatechange 檢測成功。
- responseText:用于取得響應(yīng)內(nèi)容。
- send():用于發(fā)送請求。
??以上成員都包含在 createCORSRequest() 函數(shù)返回的對象中,在所有瀏覽器中都能正常使用。
5、其他跨域技術(shù)
??在 CORS 出現(xiàn)以前,要實現(xiàn)跨域 Ajax 通信頗費一些周折。開發(fā)人員想出了一些辦法,利用 DOM 中能夠執(zhí)行跨域請求的功能,在不依賴 XHR 對象的情況下也能發(fā)送某種請求。
??雖然 CORS 技術(shù)已經(jīng)無處不在,但開發(fā)人員自己發(fā)明的這些技術(shù)仍然被廣泛使用,畢竟這樣不需要修改服務(wù)器端代碼。
5.1、圖像 Ping
??上述第一種跨域請求技術(shù)是使用<img>標簽。我們知道,一個網(wǎng)頁可以從任何網(wǎng)頁中加載圖像,不用擔(dān)心跨域不跨域。這也是在線廣告跟蹤瀏覽量的主要方式。
??可以動態(tài)地創(chuàng)建圖像,使用它們的 onload 和 onerror 事件處理程序來確定是否接收到了響應(yīng)。
??動態(tài)創(chuàng)建圖像經(jīng)常用于圖像 Ping。圖像 Ping 是與服務(wù)器進行簡單、單向的跨域通信的一種方式。請求的數(shù)據(jù)是通過查詢字符串形式發(fā)送的,而響應(yīng)可以是任意內(nèi)容,但通常是像素圖或 204 響應(yīng)。
??通過圖像 Ping,瀏覽器得不到任何具體的數(shù)據(jù),但通過偵聽 load 和 error 事件,它能知道響應(yīng)是什么時候接收到的。來看下面的例子。
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";
??這里創(chuàng)建了一個 Image 的實例,然后將 onload 和 onerror 事件處理程序指定為同一個函數(shù)。這樣無論是什么響應(yīng),只要請求完成,就能得到通知。請求從設(shè)置 src 屬性那一刻開始,而這個例子在請求中發(fā)送了一個 name 參數(shù)。
??圖像 Ping 最常用于跟蹤用戶點擊頁面或動態(tài)廣告曝光次數(shù)。圖像 Ping 有兩個主要的缺點,一是只能發(fā)送 GET 請求,二是無法訪問服務(wù)器的響應(yīng)文本。因此,圖像 Ping 只能用于瀏覽器與服務(wù)器間的單
向通信。
5.2、JSONP
??JSONP 是 JSON with padding(填充式 JSON 或參數(shù)式 JSON)的簡寫,是應(yīng)用 JSON 的一種新方法,在后來的 Web 服務(wù)中非常流行。
??JSONP 看起來與 JSON 差不多,只不過是被包含在函數(shù)調(diào)用中的 JSON,就像下面這樣。
callback({ "name": "Nicholas" });
??JSONP 由兩部分組成:回調(diào)函數(shù)和數(shù)據(jù)?;卣{(diào)函數(shù)是當(dāng)響應(yīng)到來時應(yīng)該在頁面中調(diào)用的函數(shù)。
??回調(diào)函數(shù)的名字一般是在請求中指定的。而數(shù)據(jù)就是傳入回調(diào)函數(shù)中的 JSON 數(shù)據(jù)。下面是一個典型的 JSONP 請求。
http://freegeoip.net/json/?callback=handleResponse
??這個 URL 是在請求一個 JSONP 地理定位服務(wù)。通過查詢字符串來指定 JSONP 服務(wù)的回調(diào)參數(shù)是很常見的,就像上面的 URL 所示,這里指定的回調(diào)函數(shù)的名字叫 handleResponse()。
??JSONP 是通過動態(tài)<script>元素來使用的,使用時可以為 src 屬性指定一個跨域 URL。
??這里的<script>元素與<img>元素類似,都有能力不受限制地從其他域加載資源。因為 JSONP 是有效的 JavaScript 代碼,所以在請求完成后,即在 JSONP 響應(yīng)加載到頁面中以后,就會立即執(zhí)行。來看一個例子。
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);
??上述例子通過查詢地理定位服務(wù)來顯示你的 IP 地址和位置信息。
??JSONP 之所以在開發(fā)人員中極為流行,主要原因是它非常簡單易用。與圖像 Ping 相比,它的優(yōu)點在于能夠直接訪問響應(yīng)文本,支持在瀏覽器與服務(wù)器之間雙向通信。
??不過,JSONP 也有兩點不足。
??首先,JSONP 是從其他域中加載代碼執(zhí)行。如果其他域不安全,很可能會在響應(yīng)中夾帶一些惡意代碼,而此時除了完全放棄 JSONP 調(diào)用之外,沒有辦法追究。因此在使用不是你自己運維的 Web 服務(wù)時,一定得保證它安全可靠。
??其次,要確定 JSONP 請求是否失敗并不容易。雖然 HTML5 給<script>元素新增了一個 onerror 事件處理程序,但目前還沒有得到任何瀏覽器支持。為此,開發(fā)人員不得不使用計時器檢測指定時間內(nèi)是否接收到了響應(yīng)。但就算這樣也不能盡如人意,畢竟不是每個用戶上網(wǎng)的速度和帶寬都一樣。
5.3、Comet
??Comet 是 Alex Russell( Alex Russell 是著名 JavaScript 框架 Dojo 的創(chuàng)始人。)發(fā)明的一個詞兒,指的是一種更高級的 Ajax 技術(shù)(經(jīng)常也有人稱為“服務(wù)器推送”)。
??Ajax 是一種從頁面向服務(wù)器請求數(shù)據(jù)的技術(shù),而 Comet 則是一種服務(wù)器向頁面推送數(shù)據(jù)的技術(shù)。
??Comet 能夠讓信息近乎實時地被推送到頁面上,非常適合處理體育比賽的分數(shù)和股票報價。
??有兩種實現(xiàn) Comet 的方式:長輪詢 和 流。
??長輪詢是傳統(tǒng)輪詢(也稱為短輪詢)的一個翻版,即瀏覽器定時向服務(wù)器發(fā)送請求,看有沒有更新的數(shù)據(jù)。下圖展示的是短輪詢的時間線。

??長輪詢把短輪詢顛倒了一下。頁面發(fā)起一個到服務(wù)器的請求,然后服務(wù)器一直保持連接打開,直到有數(shù)據(jù)可發(fā)送。發(fā)送完數(shù)據(jù)之后,瀏覽器關(guān)閉連接,隨即又發(fā)起一個到服務(wù)器的新請求。這一過程在頁
面打開期間一直持續(xù)不斷。下圖展示了長輪詢的時間線。

??無論是短輪詢還是長輪詢,瀏覽器都要在接收數(shù)據(jù)之前,先發(fā)起對服務(wù)器的連接。兩者最大的區(qū)別在于服務(wù)器如何發(fā)送數(shù)據(jù)。短輪詢是服務(wù)器立即發(fā)送響應(yīng),無論數(shù)據(jù)是否有效,而長輪詢是等待發(fā)送響應(yīng)。
??輪詢的優(yōu)勢是所有瀏覽器都支持,因為使用 XHR 對象和 setTimeout() 就能實現(xiàn)。而你要做的就是決定什么時候發(fā)送請求。
??第二種流行的 Comet 實現(xiàn)是** HTTP 流**。流不同于上述兩種輪詢,因為它在頁面的整個生命周期內(nèi)只使用一個 HTTP 連接。具體來說,就是瀏覽器向服務(wù)器發(fā)送一個請求,而服務(wù)器保持連接打開,然后周期性地向瀏覽器發(fā)送數(shù)據(jù)。比如,下面這段 PHP 腳本就是采用流實現(xiàn)的服務(wù)器中常見的形式。
<?php
$i = 0;
while(true){
// 輸出一些數(shù)據(jù),然后立即刷新輸出緩存
echo "Number is $i";
flush();
// 等幾秒鐘
sleep(10);
$i++;
}
??所有服務(wù)器端語言都支持打印到輸出緩存然后刷新(將輸出緩存中的內(nèi)容一次性全部發(fā)送到客戶端)的功能。而這正是實現(xiàn) HTTP 流的關(guān)鍵所在。
??在 Firefox、Safari、Opera 和 Chrome 中,通過偵聽 readystatechange 事件及檢測 readyState 的值是否為 3,就可以利用 XHR 對象實現(xiàn) HTTP 流。
??在上述這些瀏覽器中,隨著不斷從服務(wù)器接收數(shù)據(jù),readyState 的值會周期性地變?yōu)?3。當(dāng) readyState 值變?yōu)?3 時,responseText 屬性中就會保存接收到的所有數(shù)據(jù)。此時,就需要比較此前接收到的數(shù)據(jù),決定從什么位置開始取得最新的數(shù)據(jù)。使用 XHR 對象實現(xiàn) HTTP 流的典型代碼如下所示。
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ù)據(jù)并調(diào)整計數(shù)器
result = xhr.responseText.substring(received);
received += result.length;
// 調(diào)用 progress 回調(diào)函數(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!");
});
??這個 createStreamingClient() 函數(shù)接收三個參數(shù):要連接的 URL、在接收到數(shù)據(jù)時調(diào)用的函數(shù)以及關(guān)閉連接時調(diào)用的函數(shù)。
??有時候,當(dāng)連接關(guān)閉時,很可能還需要重新建立,所以關(guān)注連接什么時候關(guān)閉還是有必要的。
??只要 readystatechange 事件發(fā)生,而且 readyState 值為 3,就對 responseText 進行分割以取得最新數(shù)據(jù)。這里的 received 變量用于記錄已經(jīng)處理了多少個字符,每次 readyState 值為 3 時都遞增。
??然后,通過 progress 回調(diào)函數(shù)來處理傳入的新數(shù)據(jù)。而當(dāng) readyState 值為 4 時,則執(zhí)行 finished 回調(diào)函數(shù),傳入響應(yīng)返回的全部內(nèi)容。
??雖然這個例子比較簡單,而且也能在大多數(shù)瀏覽器中正常運行(IE 除外),但管理 Comet 的連接是很容易出錯的,需要時間不斷改進才能達到完美。瀏覽器社區(qū)認為 Comet 是未來 Web 的一個重要組成
部分,為了簡化這一技術(shù),又為 Comet 創(chuàng)建了兩個新的接口。
5.4、服務(wù)器發(fā)送事件
??SSE(Server-Sent Events,服務(wù)器發(fā)送事件)是圍繞只讀 Comet 交互推出的 API 或者模式。
??SSE API 用于創(chuàng)建到服務(wù)器的單向連接,服務(wù)器通過這個連接可以發(fā)送任意數(shù)量的數(shù)據(jù)。
??服務(wù)器響應(yīng)的 MIME 類型必須是 text/event-stream,而且是瀏覽器中的 JavaScript API 能解析格式輸出。
??SSE 支持短輪詢、長輪詢和 HTTP 流,而且能在斷開連接時自動確定何時重新連接。有了這么簡單實用的 API,再實現(xiàn) Comet 就容易多了。
??支持 SSE 的瀏覽器有 Firefox 6+、Safari 5+、Opera 11+、Chrome 和 iOS 4+版 Safari。
1. SSE API
??SSE 的 JavaScript API 與其他傳遞消息的 JavaScript API 很相似。要預(yù)訂新的事件流,首先要創(chuàng)建一個新的 EventSource 對象,并傳進一個入口點:
var source = new EventSource("myevents.php");
??注意,傳入的 URL 必須與創(chuàng)建對象的頁面同源(相同的 URL 模式、域及端口)。EventSource 的實例有一個 readyState 屬性,值為 0 表示正連接到服務(wù)器,值為 1 表示打開了連接,值為 2 表示關(guān)閉
了連接。另外,還有以下三個事件。
- open:在建立連接時觸發(fā)。
- message:在從服務(wù)器接收到新事件時觸發(fā)。
- error:在無法建立連接時觸發(fā)。
??就一般的用法而言,onmessage 事件處理程序也沒有什么特別的。
source.onmessage = function(event){
var data = event.data;
// 處理數(shù)據(jù)
};
??服務(wù)器發(fā)回的數(shù)據(jù)以字符串形式保存在 event.data 中。
??默認情況下,EventSource 對象會保持與服務(wù)器的活動連接。如果連接斷開,還會重新連接。這就意味著 SSE 適合長輪詢和 HTTP 流。如果想強制立即斷開連接并且不再重新連接,可以調(diào)用 close() 方法。
source.close();
2. 事件流
??所謂的服務(wù)器事件會通過一個持久的 HTTP 響應(yīng)發(fā)送,這個響應(yīng)的 MIME 類型為 text/event-stream。響應(yīng)的格式是純文本,最簡單的情況是每個數(shù)據(jù)項都帶有前綴 data:,例如:
data: foo
data: bar
data: foo
data: bar
??對以上響應(yīng)而言,事件流中的第一個 message 事件返回的 event.data 值為"foo",第二個 message 事件返回的 event.data 值為"bar",第三個 message 事件返回的 event.data 值為 "foo\nbar"(注意中間的換行符)。
??對于多個連續(xù)的以 data:開頭的數(shù)據(jù)行,將作為多段數(shù)據(jù)解析,每個值之間以一個換行符分隔。只有在包含 data:的數(shù)據(jù)行后面有空行時,才會觸發(fā) message 事件,因此在服務(wù)器上生成事件流時不能忘了多添加這一行。
??通過 id:前綴可以給特定的事件指定一個關(guān)聯(lián)的 ID,這個 ID 行位于 data:行前面或后面皆可:
data: foo
id: 1
??設(shè)置了 ID 后,EventSource 對象會跟蹤上一次觸發(fā)的事件。如果連接斷開,會向服務(wù)器發(fā)送一個包含名為 Last-Event-ID 的特殊 HTTP 頭部的請求,以便服務(wù)器知道下一次該觸發(fā)哪個事件。在多次連接的事件流中,這種機制可以確保瀏覽器以正確的順序收到連接的數(shù)據(jù)段。
5.5、Web Sockets
??要說最令人津津樂道的新瀏覽器 API,就得數(shù) Web Sockets 了。Web Sockets 的目標是在一個單獨的持久連接上提供全雙工、雙向通信。
??在 JavaScript 中創(chuàng)建了 Web Socket 之后,會有一個 HTTP 請求發(fā)送到瀏覽器以發(fā)起連接。在取得服務(wù)器響應(yīng)后,建立的連接會使用 HTTP 升級從 HTTP 協(xié)議交換為 Web Socket 協(xié)議。
??也就是說,使用標準的 HTTP 服務(wù)器無法實現(xiàn) Web Sockets,只有支持這種協(xié)議的專門服務(wù)器才能正常工作。
??由于 Web Sockets 使用了自定義的協(xié)議,所以 URL 模式也略有不同。未加密的連接不再是 http://,而是 ws://;加密的連接也不是 https://,而是 wss://。
??在使用 Web Socket URL 時,必須帶著這個模式,因為將來還有可能支持其他模式。
??使用自定義協(xié)議而非 HTTP 協(xié)議的好處是,能夠在客戶端和服務(wù)器之間發(fā)送非常少量的數(shù)據(jù),而不必擔(dān)心 HTTP 那樣字節(jié)級的開銷。由于傳遞的數(shù)據(jù)包很小,因此 Web Sockets 非常適合移動應(yīng)用。畢竟
對移動應(yīng)用而言,帶寬和網(wǎng)絡(luò)延遲都是關(guān)鍵問題。
??使用自定義協(xié)議的缺點在于,制定協(xié)議的時間比制定 JavaScript API 的時間還要長。
??Web Sockets 曾幾度擱淺,就因為不斷有人發(fā)現(xiàn)這個新協(xié)議存在一致性和安全性的問題。Firefox 4 和 Opera 11 都曾默認啟用 Web Sockets,但在發(fā)布前夕又禁用了,因為又發(fā)現(xiàn)了安全隱患。目前支持 Web Sockets 的瀏覽器有 Firefox 6+、Safari 5+、Chrome 和 iOS 4+版 Safari。
1. Web Sockets API
??要創(chuàng)建 Web Socket,先實例一個 WebSocket 對象并傳入要連接的 URL:
var socket = new WebSocket("ws://www.example.com/server.php");
??注意,必須給 WebSocket 構(gòu)造函數(shù)傳入絕對 URL。同源策略對 Web Sockets 不適用,因此可以通過它打開到任何站點的連接。至于是否會與某個域中的頁面通信,則完全取決于服務(wù)器。(通過握手信
息就可以知道請求來自何方。)
??實例化了 WebSocket 對象后,瀏覽器就會馬上嘗試創(chuàng)建連接。與 XHR 類似,WebSocket 也有一個表示當(dāng)前狀態(tài)的 readyState 屬性。不過,這個屬性的值與 XHR 并不相同,而是如下所示。
- WebSocket.OPENING (0):正在建立連接。
- WebSocket.OPEN (1):已經(jīng)建立連接。
- WebSocket.CLOSING (2):正在關(guān)閉連接。
- WebSocket.CLOSE (3):已經(jīng)關(guān)閉連接。
??WebSocket 沒有 readystatechange 事件;不過,它有其他事件,對應(yīng)著不同的狀態(tài)。readyState 的值永遠從 0 開始。要關(guān)閉 Web Socket 連接,可以在任何時候調(diào)用 close() 方法。
socket.close();
??調(diào)用了 close() 之后,readyState 的值立即變?yōu)?2(正在關(guān)閉),而在關(guān)閉連接后就會變成 3。
2. 發(fā)送和接收數(shù)據(jù)
??Web Socket 打開之后,就可以通過連接發(fā)送和接收數(shù)據(jù)。要向服務(wù)器發(fā)送數(shù)據(jù),使用 send() 方法并傳入任意字符串,例如:
var socket = new WebSocket("ws://www.example.com/server.php");
socket.send("Hello world!");
??因為 Web Sockets 只能通過連接發(fā)送純文本數(shù)據(jù),所以對于復(fù)雜的數(shù)據(jù)結(jié)構(gòu),在通過連接發(fā)送之前,必須進行序列化。下面的例子展示了先將數(shù)據(jù)序列化為一個 JSON 字符串,然后再發(fā)送到服務(wù)器:
var message = {
time: new Date(),
text: "Hello world!",
clientId: "asdfp8734rew"
};
socket.send(JSON.stringify(message));
??接下來,服務(wù)器要讀取其中的數(shù)據(jù),就要解析接收到的 JSON 字符串。
??當(dāng)服務(wù)器向客戶端發(fā)來消息時,WebSocket 對象就會觸發(fā) message 事件。這個 message 事件與其他傳遞消息的協(xié)議類似,也是把返回的數(shù)據(jù)保存在 event.data 屬性中。
socket.onmessage = function(event){
var data = event.data;
// 處理數(shù)據(jù)
};
??與通過 send() 發(fā)送到服務(wù)器的數(shù)據(jù)一樣,event.data 中返回的數(shù)據(jù)也是字符串。如果你想得到其他格式的數(shù)據(jù),必須手工解析這些數(shù)據(jù)。
3. 其他事件
??WebSocket 對象還有其他三個事件,在連接生命周期的不同階段觸發(fā)。
- open:在成功建立連接時觸發(fā)。
- error:在發(fā)生錯誤時觸發(fā),連接不能持續(xù)。
- close:在連接關(guān)閉時觸發(fā)。
??WebSocket 對象不支持 DOM 2 級事件偵聽器,因此必須使用 DOM 0 級語法分別定義每個事件處理程序。
var socket = new WebSocket("ws://www.example.com/server.php");
socket.onopen = function(){
alert("Connection established.");
};
socket.onerror = function(){
alert("Connection error.");
};
socket.onclose = function(){
alert("Connection closed.");
};
??在這三個事件中,只有 close 事件的 event 對象有額外的信息。
??close 事件的事件對象有三個額外的屬性:wasClean、code 和 reason。其中,wasClean 是一個布爾值,表示連接是否已經(jīng)明確地關(guān)
閉;code 是服務(wù)器返回的數(shù)值狀態(tài)碼;而 reason 是一個字符串,包含服務(wù)器發(fā)回的消息??梢园堰@些信息顯示給用戶,也可以記錄到日志中以便將來分析。
socket.onclose = function(event){
console.log("Was clean? " + event.wasClean + " Code=" + event.code + " Reason=" + event.reason);
};
5.6、SSE 與 Web Sockets
??面對某個具體的用例,在考慮是使用 SSE 還是使用 Web Sockets 時,可以考慮如下幾個因素。
??首先,你是否有自由度建立和維護 Web Sockets 服務(wù)器?因為 Web Socket 協(xié)議不同于 HTTP,所以現(xiàn)有服務(wù)器不能用于 Web Socket 通信。SSE 倒是通過常規(guī) HTTP 通信,因此現(xiàn)有服務(wù)器就可以滿足需求。
??第二個要考慮的問題是到底需不需要雙向通信。如果用例只需讀取服務(wù)器數(shù)據(jù)(如比賽成績),那么 SSE 比較容易實現(xiàn)。如果用例必須雙向通信(如聊天室),那么 Web Sockets 顯然更好。
??別忘了,在不能選擇 Web Sockets 的情況下,組合 XHR 和 SSE 也是能實現(xiàn)雙向通信的。
6、安全
??討論 Ajax 和 Comet 安全的文章可謂連篇累牘,而相關(guān)主題的書也已經(jīng)出了很多本了。大型 Ajax 應(yīng)用程序的安全問題涉及面非常之廣,但我們可以從普遍意義上探討一些基本的問題。
??首先,可以通過 XHR 訪問的任何 URL 也可以通過瀏覽器或服務(wù)器來訪問。下面的 URL 就是一個例子。
/getuserinfo.php?id=23
??如果是向這個 URL 發(fā)送請求,可以想象結(jié)果會返回 ID 為 23 的用戶的某些數(shù)據(jù)。誰也無法保證別人不會將這個 URL 的用戶 ID 修改為 24、56 或其他值。因此,getuserinfo.php 文件必須知道請求者是否真的有權(quán)限訪問要請求的數(shù)據(jù);否則,你的服務(wù)器就會門戶大開,任何人的數(shù)據(jù)都可能被泄漏出去。
??對于未被授權(quán)系統(tǒng)有權(quán)訪問某個資源的情況,我們稱之為 CSRF(Cross-Site Request Forgery,跨站點請求偽造)。未被授權(quán)系統(tǒng)會偽裝自己,讓處理請求的服務(wù)器認為它是合法的。
??受到 CSRF 攻擊的 Ajax 程序有大有小,攻擊行為既有旨在揭示系統(tǒng)漏洞的惡作劇,也有惡意的數(shù)據(jù)竊取或數(shù)據(jù)銷毀。
??為確保通過 XHR 訪問的 URL 安全,通行的做法就是驗證發(fā)送請求者是否有權(quán)限訪問相應(yīng)的資源。有下列幾種方式可供選擇。
- 要求以 SSL 連接來訪問可以通過 XHR 請求的資源。
- 要求每一次請求都要附帶經(jīng)過相應(yīng)算法計算得到的驗證碼。
??請注意,下列措施對防范 CSRF 攻擊不起作用。
- 要求發(fā)送 POST 而不是 GET 請求——很容易改變。
- 檢查來源 URL 以確定是否可信——來源記錄很容易偽造。
- 基于 cookie 信息進行驗證——同樣很容易偽造。
??XHR 對象也提供了一些安全機制,雖然表面上看可以保證安全,但實際上卻相當(dāng)不可靠。
??實際上,前面介紹的 open() 方法還能再接收兩個參數(shù):要隨請求一起發(fā)送的用戶名和密碼。帶有這兩個參數(shù)的請求可以通過 SSL 發(fā)送給服務(wù)器上的頁面,如下面的例子所示。
xhr.open("get", "example.php", true, "username", "password"); // 不要這樣做?。?
??即便可以考慮這種安全機制,但還是盡量不要這樣做。把用戶名和密碼保存在 JavaScript 代碼中本身就是極為不安全的。任何人,只要他會使用 JavaScript 調(diào)試器,就可以通過查看相應(yīng)的變量發(fā)現(xiàn)純文本形式的用戶名和密碼。
小結(jié)
??Ajax 是無需刷新頁面就能夠從服務(wù)器取得數(shù)據(jù)的一種方法。關(guān)于 Ajax,可以從以下幾方面來總結(jié)一下。
- 負責(zé) Ajax 運作的核心對象是 XMLHttpRequest(XHR)對象。
- XHR 對象由微軟最早在 IE5 中引入,用于通過 JavaScript 從服務(wù)器取得 XML 數(shù)據(jù)。
- 在此之后,F(xiàn)irefox、Safari、Chrome 和 Opera 都實現(xiàn)了相同的特性,使 XHR 成為了 Web 的一個事實標準。
- 雖然實現(xiàn)之間存在差異,但 XHR 對象的基本用法在不同瀏覽器間還是相對規(guī)范的,因此可以放心地用在 Web 開發(fā)當(dāng)中。
??同源策略是對 XHR 的一個主要約束,它為通信設(shè)置了“相同的域、相同的端口、相同的協(xié)議”這一限制。試圖訪問上述限制之外的資源,都會引發(fā)安全錯誤,除非采用被認可的跨域解決方案。
??這個解決方案叫做 CORS(Cross-Origin Resource Sharing,跨源資源共享),IE8 通過 XDomainRequest 對象支持 CORS,其他瀏覽器通過 XHR 對象原生支持 CORS。圖像 Ping 和 JSONP 是另外兩種跨域通信的技術(shù),但不如 CORS 穩(wěn)妥。
??Comet 是對 Ajax 的進一步擴展,讓服務(wù)器幾乎能夠?qū)崟r地向客戶端推送數(shù)據(jù)。實現(xiàn) Comet 的手段主要有兩個:長輪詢和 HTTP 流。所有瀏覽器都支持長輪詢,而只有部分瀏覽器原生支持 HTTP 流。
??SSE(Server-Sent Events,服務(wù)器發(fā)送事件)是一種實現(xiàn) Comet 交互的瀏覽器 API,既支持長輪詢,也支持 HTTP 流。
??Web Sockets 是一種與服務(wù)器進行全雙工、雙向通信的信道。與其他方案不同,Web Sockets 不使用 HTTP 協(xié)議,而使用一種自定義的協(xié)議。這種協(xié)議專門為快速傳輸小數(shù)據(jù)設(shè)計。雖然要求使用不同的 Web 服務(wù)器,但卻具有速度上的優(yōu)勢。
??各方面對 Ajax 和 Comet 的鼓吹吸引了越來越多的開發(fā)人員學(xué)習(xí) JavaScript,人們對 Web 開發(fā)的關(guān)注也再度升溫。與 Ajax 有關(guān)的概念都還相對比較新,這些概念會隨著時間推移繼續(xù)發(fā)展。