??2005 年,Jesse James Garrett 發表了一篇在線文章,題為“Ajax: A new Approach to Web Applications” 。他在這篇文章里介紹了一種技術,用他的話說,就叫 Ajax,是對 Asynchronous JavaScript + XML 的簡寫。
(http://www.adaptivepath.com/ideas/essays/archives/000385.php)
??這一技術能夠像服務器請求額外的數據而無需卸載頁面,會帶來更好的用戶體驗。Garrett 還解釋了怎樣使用這一技術改變自從 Web 誕生以來就一直沿用的“單擊”,“等待”的交互模式。
??Ajax 技術的核心是 XMLHttpRequest 對象(簡稱 XHR),這是由微軟首先引入的一個特性,其他瀏覽器提供商后來都提供了相同的實現。
??在 XHR 出現之前,Ajax 式的通信必須借助一些 hack 手段來實現,大多數是使用隱藏的框架或內嵌框架。
??XHR 為向服務器發送請求和解析服務器響應提供了流暢的接口。能夠以異步方式從服務器取得更多信息,意味著用戶單擊后,可以不必刷新頁面也能取得新數據。
??也就是說,可以使用 XHR 對象取得新數據,然后再通過 DOM 將新數據插入到頁面中。另外,雖然名字中包含 XML 的成分,但 Ajax 通信與數據格式無關;這種技術就是無須刷新頁面即可從服務器取得數據,但不一定是 XML 數據。
??實際上,Garrett 提到的這種技術已經存在很長時間了。在 Garrett 撰寫那篇文章之前,人們通常將這種技術叫做遠程腳本(remote scripting),而且早在 1998 年就有人采用不同的手段實現了這種瀏覽器與服務器的通信。再往前推,JavaScript 需要通過 Java applet 或 Flash 電影等中間層向服務器發送請求。
??而 XHR 則將瀏覽器原生的通信能力提供給了開發人員,簡化了實現同樣操作的任務。
??在重命名為 Ajax 之后,大約是 2005 年底 2006 年初,這種瀏覽器與服務器的通信技術可謂紅極一時。人們對 JavaScript 和 Web 的全新認識,催生了很多使用原有特性的新技術和新模式。
??就目前來說,熟練使用 XHR 對象已經成為所有 Web 開發人員必須掌握的一種技能。
1、XMLHttpRequest 對象
??IE5 是第一款引入 XHR 對象的瀏覽器。在 IE5 中,XHR 對象是通過 MSXML 庫中的一個 ActiveX 對象實現的。因此,在 IE 中可能會遇到三種不同版本的 XHR 對象,即 MSXML2.XMLHttp、MSXML2.XMLHttp.3.0 和 MXSML2.XMLHttp.6.0。
??IE7+、Firefox、Opera、Chrome 和 Safari 都支持原生的 XHR 對象,在這些瀏覽器中創建 XHR 對象要像下面這樣使用 XMLHttpRequest 構造函數。
var xhr = new XMLHttpRequest();
??如果你必須還要支持 IE 的早期版本,那么則可以使用下面這個函數。
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.");
}
}
??這個函數中新增的代碼首先檢測原生 XHR 對象是否存在,如果存在則返回它的新實例。如果原生對象不存在,則檢測 ActiveX 對象。如果這兩種對象都不存在,就拋出一個錯誤。
??然后,就可以使用下面的代碼在所有瀏覽器中創建 XHR 對象了。
var xhr = createXHR();
??由于其他瀏覽器中對 XHR 的實現與 IE 最早的實現是兼容的,因此就可以在所有瀏覽器中都以相同方式使用上面創建的 xhr 對象。
1.1、XHR 的用法
??在使用 XHR 對象時,要調用的第一個方法是 open(),它接受 3 個參數:要發送的請求的類型("get"、"post"等)、請求的 URL 和表示是否異步發送請求的布爾值。下面就是調用這個方法的例子。
xhr.open("get", "example.php", false);
??這行代碼會啟動一個針對 example.php 的 GET 請求。有關這行代碼,需要說明兩點:一是 URL 相對于執行代碼的當前頁面(當然也可以使用絕對路徑);二是調用 open() 方法并不會真正發送請求,而只是啟動一個請求以備發送。
??只能向同一個域中使用相同端口和協議的 URL 發送請求。如果 URL 與啟動請求的頁面有任何差別,都會引發安全錯誤。
??要發送特定的請求,必須像下面這樣調用 send() 方法:
xhr.open("get", "example.txt", false);
xhr.send(null);
??這里的 send() 方法接收一個參數,即要作為請求主體發送的數據。如果不需要通過請求主體發送數據,則必須傳入 null,因為這個參數對有些瀏覽器來說是必需的。
??調用 send() 之后,請求就會被分派到服務器。由于這次請求是同步的,JavaScript 代碼會等到服務器響應之后再繼續執行。在收到響應后,響應的數據會自動填充 XHR 對象的屬性,相關的屬性簡介如下。
- responseText:作為響應主體被返回的文本。
- responseXML:如果響應的內容類型是"text/xml"或"application/xml",這個屬性中將保存包含著響應數據的 XML DOM 文檔。
- status:響應的 HTTP 狀態。
- statusText:HTTP 狀態的說明。
??在接收到響應后,第一步是檢查 status 屬性,以確定響應已經成功返回。一般來說,可以將 HTTP 狀態代碼為 200 作為成功的標志。此時,responseText 屬性的內容已經就緒,而且在內容類型正確的情況下,responseXML 也應該能夠訪問了。
??此外,狀態代碼為 304 表示請求的資源并沒有被修改,可以直接使用瀏覽器中緩存的版本;當然,也意味著響應是有效的。為確保接收到適當的響應,應該像下面這樣檢查上述這兩種狀態代碼:
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);
}
??根據返回的狀態代碼,這個例子可能會顯示由服務器返回的內容,也可能會顯示一條錯誤消息。
??我們建議讀者要通過檢測 status 來決定下一步的操作,不要依賴 statusText,因為后者在跨瀏覽器使用時不太可靠。
??另外,無論內容類型是什么,響應主體的內容都會保存到 responseText 屬性中;而對于非 XML 數據而言,responseXML 屬性的值將為 null。
??有的瀏覽器會錯誤地報告 204 狀態代碼。IE 中 XHR 的 ActiveX 版本會將 204 設置為 1223,而 IE 中原生的 XHR 則會將 204 規范化為 200。Opera 會在取得 204 時報告 status 的值為 0。
??像前面這樣發送同步請求當然沒有問題,但多數情況下,我們還是要發送異步請求,才能讓 JavaScript 繼續執行而不必等待響應。此時,可以檢測 XHR 對象的 readyState 屬性,該屬性表示請求/響應過程的當前活動階段。這個屬性可取的值如下。
- 0:未初始化。尚未調用 open() 方法。
- 1:啟動。已經調用 open() 方法,但尚未調用 send() 方法。
- 2:發送。已經調用 send() 方法,但尚未接收到響應。
- 3:接收。已經接收到部分響應數據。
- 4:完成。已經接收到全部響應數據,而且已經可以在客戶端使用了。
??只要 readyState 屬性的值由一個值變成另一個值,都會觸發一次 readystatechange 事件。可以利用這個事件來檢測每次狀態變化后 readyState 的值。
??通常,我們只對 readyState 值為 4 的階段感興趣,因為這時所有數據都已經就緒。不過,必須在調用 open() 之前指定 onreadystatechange 事件處理程序才能確保跨瀏覽器兼容性。下面來看一個例子。
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 對象,在有的瀏覽器中會導致函數執行失敗,或者導致錯誤發生。因此,使用實際的 XHR 對象實例變量是較為可靠的一種方式。
??另外,在接收到響應之前還可以調用 abort() 方法來取消異步請求,如下所示:
xhr.abort();
??調用這個方法后,XHR 對象會停止觸發事件,而且也不再允許訪問任何與響應有關的對象屬性。在終止請求之后,還應該對 XHR 對象進行解引用操作。由于內存原因,不建議重用 XHR 對象。
1.2、HTTP 頭部信息
??每個 HTTP 請求和響應都會帶有相應的頭部信息,其中有的對開發人員有用,有的也沒有什么用。
??XHR 對象也提供了操作這兩種頭部(即請求頭部和響應頭部)信息的方法。
??默認情況下,在發送 XHR 請求的同時,還會發送下列頭部信息。
- Accept:瀏覽器能夠處理的內容類型。
- Accept-Charset:瀏覽器能夠顯示的字符集。
- Accept-Encoding:瀏覽器能夠處理的壓縮編碼。
- Accept-Language:瀏覽器當前設置的語言。
- Connection:瀏覽器與服務器之間連接的類型。
- Cookie:當前頁面設置的任何 Cookie。
- Host:發出請求的頁面所在的域 。
- Referer:發出請求的頁面的 URI。注意,HTTP 規范將這個頭部字段拼寫錯了,而為保證與規范一致,也只能將錯就錯了。(這個英文單詞的正確拼法應該是 referrer。)
- User-Agent:瀏覽器的用戶代理字符串。
??雖然不同瀏覽器實際發送的頭部信息會有所不同,但以上列出的基本上是所有瀏覽器都會發送的。
??使用 setRequestHeader() 方法可以設置自定義的請求頭部信息。這個方法接受兩個參數:頭部字段的名稱和頭部字段的值。
??要成功發送請求頭部信息,必須在調用 open() 方法之后且調用 send() 方法之前調用 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);
??服務器在接收到這種自定義的頭部信息之后,可以執行相應的后續操作。我們建議讀者使用自定義的頭部字段名稱,不要使用瀏覽器正常發送的字段名稱,否則有可能會影響服務器的響應。有的瀏覽器允許開發人員重寫默認的頭部信息,但有的覽器則不允許這樣做。
??調用 XHR 對象的 getResponseHeader() 方法并傳入頭部字段名稱,可以取得相應的響應頭部信息。而調用 getAllResponseHeaders() 方法則可以取得一個包含所有頭部信息的長字符串。來看下面的例子。
var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllResponseHeaders();
??在服務器端,也可以利用頭部信息向瀏覽器發送額外的、結構化的數據。在沒有自定義信息的情況下,getAllResponseHeaders() 方法通常會返回如下所示的多行文本內容:
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
??這種格式化的輸出可以方便我們檢查響應中所有頭部字段的名稱,而不必一個一個地檢查某個字段是否存在。
1.3、GET 請求
??GET 是最常見的請求類型,最常用于向服務器查詢某些信息。必要時,可以將查詢字符串參數追加到 URL 的末尾,以便將信息發送給服務器。
??對 XHR 而言,位于傳入 open() 方法的 URL 末尾的查詢字符串必須經過正確的編碼才行。
??使用 GET 請求經常會發生的一個錯誤,就是查詢字符串的格式有問題。查詢字符串中每個參數的名稱和值都必須使用 encodeURIComponent() 進行編碼,然后才能放到 URL 的末尾;而且所有名-值對兒都必須由和號(&)分隔,如下面的例子所示。
xhr.open("get", "example.php?name1=value1&name2=value2", true);
??下面這個函數可以輔助向現有 URL 的末尾添加查詢字符串參數:
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
??這個 addURLParam() 函數接受三個參數:要添加參數的 URL、參數的名稱和參數的值。
??這個函數首先檢查 URL 是否包含問號(以確定是否已經有參數存在)。如果沒有,就添加一個問號;否則,就添加一個和號。然后,將參數名稱和值進行編碼,再添加到 URL 的末尾。最后返回添加參數之后的 URL。
??下面是使用這個函數來構建請求 URL 的示例。
var url = "example.php";
// 添加參數
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
//初始化請求
xhr.open("get", url, false);
??在這里使用 addURLParam() 函數可以確保查詢字符串的格式良好,并可靠地用于 XHR 對象。
1.4、POST 請求
??使用頻率僅次于 GET 的是 POST 請求,通常用于向服務器發送應該被保存的數據。
??POST 請求應該把數據作為請求的主體提交,而 GET 請求傳統上不是這樣。
??POST 請求的主體可以包含非常多的數據,而且格式不限。
??在 open() 方法第一個參數的位置傳入"post",就可以初始化一個 POST 請求,如下面的例子所示。
xhr.open("post", "example.php", true);
??發送 POST 請求的第二步就是向 send() 方法中傳入某些數據。由于 XHR 最初的設計主要是為了處理 XML,因此可以在此傳入 XML DOM 文檔,傳入的文檔經序列化之后將作為請求主體被提交到服務器。
??當然,也可以在此傳入任何想發送到服務器的字符串。
??默認情況下,服務器對 POST 請求和提交 Web 表單的請求并不會一視同仁。因此,服務器端必須有程序來讀取發送過來的原始數據,并從中解析出有用的部分。
??不過,我們可以使用 XHR 來模仿表單提交:首先將 Content-Type 頭部信息設置為 application/x-www-form-urlencoded,也就是表單提交時的內容類型,其次是以適當的格式創建一個字符串。POST 數據的格式與查詢字符串格式相同。如果需要將頁面中表單的數據進行序列化,然后再通過 XHR 發送到服務器,那么就可以使用 serialize() 函數來創建這個字符串:
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": //復選框
if (!field.checked){
break;
}
/* 執行默認操作 */
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));
}
??這個函數可以將 ID 為"user-info"的表單中的數據序列化之后發送給服務器。而下面的示例 PHP 文件 postexample.php 就可以通過$_POST 取得提交的數據了:
<?php
header("Content-Type: text/plain");
echo <<<EOF
Name: {$_POST[‘user-name’]}
Email: {$_POST[‘user-email’]}
EOF;
?>
??如果不設置 Content-Type 頭部信息,那么發送給服務器的數據就不會出現在$_POST 超級全局變量中。這時候,要訪問同樣的數據,就必須借助$HTTP_RAW_POST_DATA。
??與 GET 請求相比,POST 請求消耗的資源會更多一些。從性能角度來看,以發送相同的數據計,GET 請求的速度最多可達到 POST 請求的兩倍。
2、XMLHTtpRequest 2 級
??鑒于 XHR 已經得到廣泛接受,成為了事實標準,W3C 也著手制定相應的標準以規范其行為。
XMLHttpRequest 1 級只是把已有的 XHR 對象的實現細節描述了出來。而 XMLHttpRequest 2 級則進一步發展了 XHR。
??并非所有瀏覽器都完整地實現了 XMLHttpRequest 2 級規范,但所有瀏覽器都實現了它規定的部分內容。
2.1、FormData
??現代 Web 應用中頻繁使用的一項功能就是表單數據的序列化,XMLHttpRequest 2 級為此定義了 FormData 類型。
??FormData 為序列化表單以及創建與表單格式相同的數據(用于通過 XHR 傳輸)提供了便利。下面的代碼創建了一個 FormData 對象,并向其中添加了一些數據。
var data = new FormData();
data.append("name", "Nicholas");
??這個 append() 方法接收兩個參數:鍵和值,分別對應表單字段的名字和字段中包含的值。可以像這樣添加任意多個鍵值對兒。
??而通過向 FormData 構造函數中傳入表單元素,也可以用表單元素的數據預先向其中填入鍵值對兒:
var data = new FormData(document.forms[0]);
??創建了 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 的方便之處體現在不必明確地在 XHR 對象上設置請求頭部。XHR 對象能夠識別傳入的數據類型是 FormData 的實例,并配置適當的頭部信息。
??支持 FormData 的瀏覽器有 Firefox 4+、Safari 5+、Chrome 和 Android 3+版 WebKit。
2.2、超時設定
??IE8 為 XHR 對象添加了一個 timeout 屬性,表示請求在等待響應多少毫秒之后就終止。在給 timeout 設置一個數值后,如果在規定的時間內瀏覽器還沒有接收到響應,那么就會觸發 timeout 事件,進而會調用 ontimeout 事件處理程序。這項功能后來也被收入了 XMLHttpRequest 2 級規范中。來看下面的例子。
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){
// 假設由 ontimeout 事件處理程序處理
}
}
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // 將超時設置為 1 秒鐘(僅適用于 IE8+)
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);
??這個例子示范了如何使用 timeout 屬性。將這個屬性設置為 1000 毫秒,意味著如果請求在 1 秒鐘內還沒有返回,就會自動終止。請求終止時,會調用 ontimeout 事件處理程序。
??但此時 readyState 可能已經改變為 4 了,這意味著會調用 onreadystatechange 事件處理程序。
??可是,如果在超時終止請求之后再訪問 status 屬性,就會導致錯誤。為避免瀏覽器報告錯誤,可以將檢查 status 屬性的語句封裝在一個 try-catch 語句當中。
2.3、overrideMimeType() 方法
??Firefox 最早引入了 overrideMimeType() 方法,用于重寫 XHR 響應的 MIME 類型。這個方法后來也被納入了 XMLHttpRequest 2 級規范。
??因為返回響應的 MIME 類型決定了 XHR 對象如何處理它,所以提供一種方法能夠重寫服務器返回的 MIME 類型是很有用的。比如,服務器返回的 MIME 類型是 text/plain,但數據中實際包含的是 XML。根據 MIME 類型,即使數據是 XML,responseXML 屬性中仍然是 null。通過調用 overrideMimeType() 方法,可以保證把響應當作 XML 而非純文本來處理。
var xhr = createXHR();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
??這個例子強迫 XHR 對象將響應當作 XML 而非純文本來處理。調用 overrideMimeType() 必須在 send()方法之前,才能保證重寫響應的 MIME 類型。
??支持 overrideMimeType() 方法的瀏覽器有 Firefox、Safari 4+、Opera 10.5 和 Chrome。
3、進度事件
??Progress Events 規范是 W3C 的一個工作草案,定義了與客戶端服務器通信有關的事件。這些事件最早其實只針對 XHR 操作,但目前也被其他 API 借鑒。有以下 6 個進度事件。
- loadstart:在接收到響應數據的第一個字節時觸發。
- progress:在接收響應期間持續不斷地觸發。
- error:在請求發生錯誤時觸發。
- abort:在因為調用 abort() 方法而終止連接時觸發。
- load:在接收到完整的響應數據時觸發。
- loadend:在通信完成或者觸發 error、abort 或 load 事件后觸發。
??每個請求都從觸發 loadstart 事件開始,接下來是一或多個 progress 事件,然后觸發 error、abort 或 load 事件中的一個,最后以觸發 loadend 事件結束。
??支持前 5 個事件的瀏覽器有 Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 版 WebKit。Opera(從第 11 版開始)、IE 8+只支持 load 事件。目前還沒有瀏覽器支持 loadend 事件。這些事件大都很直觀,但其中兩個事件有一些細節需要注意。
3.1、load 事件
??Firefox 在實現 XHR 對象的某個版本時,曾致力于簡化異步交互模型。最終,Firefox 實現中引入了 load 事件,用以替代 readystatechange 事件。響應接收完畢后將觸發 load 事件,因此也就沒有必要去檢查 readyState 屬性了。
??而 onload 事件處理程序會接收到一個 event 對象,其 target 屬性就指向 XHR 對象實例,因而可以訪問到 XHR 對象的所有方法和屬性。
??然而,并非所有瀏覽器都為這個事件實現了適當的事件對象。結果,開發人員還是要像下面這樣被迫使用 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);
??只要瀏覽器接收到服務器的響應,不管其狀態如何,都會觸發 load 事件。而這意味著你必須要檢查 status 屬性,才能確定數據是否真的已經可用了。Firefox、Opera、Chrome 和 Safari 都支持 load 事件。
3.2、progress 事件
??Mozilla 對 XHR 的另一個革新是添加了 progress 事件,這個事件會在瀏覽器接收新數據期間周期性地觸發。
??而 onprogress 事件處理程序會接收到一個 event 對象,其 target 屬性是 XHR 對象,但包含著三個額外的屬性:lengthComputable、position 和 totalSize。
??其中,lengthComputable 是一個表示進度信息是否可用的布爾值,position 表示已經接收的字節數,totalSize 表示根據 Content-Length 響應頭部確定的預期字節數。有了這些信息,我們就可以為用戶創建一個進度指示器了。下面展示了為用戶創建進度指示器的一個示例。
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);
??為確保正常執行,必須在調用 open() 方法之前添加 onprogress 事件處理程序。在前面的例子中,每次觸發 progress 事件,都會以新的狀態信息更新 HTML 元素的內容。如果響應頭部中包含 Content-Length 字段,那么也可以利用此信息來計算從響應中已經接收到的數據的百分比。
4、跨源資源共享
??通過 XHR 實現 Ajax 通信的一個主要限制,來源于跨域安全策略。默認情況下,XHR 對象只能訪問與包含它的頁面位于同一個域中的資源。這種安全策略可以預防某些惡意行為。
??但是,實現合理的跨域請求對開發某些瀏覽器應用程序也是至關重要的。
??CORS(Cross-Origin Resource Sharing,跨源資源共享)是 W3C 的一個工作草案,定義了在必須訪問跨源資源時,瀏覽器與服務器應該如何溝通。
??CORS 背后的基本思想,就是使用自定義的 HTTP 頭部讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。
??比如一個簡單的使用 GET 或 POST 發送的請求,它沒有自定義的頭部,而主體內容是 text/plain。在發送該請求時,需要給它附加一個額外的 Origin 頭部,其中包含請求頁面的源信息(協議、域名和端口),以便服務器根據這個頭部信息來決定是否給予響應。下面是 Origin 頭部的一個示例:
Origin: http://www.nczonline.net
??如果服務器認為這個請求可以接受,就在 Access-Control-Allow-Origin 頭部中回發相同的源信息(如果是公共資源,可以回發"*")。例如:
Access-Control-Allow-Origin: http://www.nczonline.net
??如果沒有這個頭部,或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。注意,請求和響應都不包含 cookie 信息。
4.1、IE 對 CORS 的實現
??微軟在 IE8 中引入了 XDR(XDomainRequest)類型。這個對象與 XHR 類似,但能實現安全可靠的跨域通信。
??XDR 對象的安全機制部分實現了 W3C 的 CORS 規范。以下是 XDR 與 XHR 的一些不同之處。
- cookie 不會隨請求發送,也不會隨響應返回。
- 只能設置請求頭部信息中的 Content-Type 字段。
- 不能訪問響應頭部信息。
- 只支持 GET 和 POST 請求。
??這些變化使 CSRF(Cross-Site Request Forgery,跨站點請求偽造)和 XSS(Cross-Site Scripting,跨站點腳本)的問題得到了緩解。
??被請求的資源可以根據它認為合適的任意數據(用戶代理、來源頁面等)來決定是否設置 Access-Control- Allow-Origin 頭部。作為請求的一部分,Origin 頭部的值表示請求的來源域,以便遠程資源明確地識別 XDR 請求。
??XDR 對象的使用方法與 XHR 對象非常相似。也是創建一個 XDomainRequest 的實例,調用 open() 方法,再調用 send() 方法。
??但與 XHR 對象的 open() 方法不同,XDR 對象的 open() 方法只接收兩個參數:請求的類型和 URL。
??所有 XDR 請求都是異步執行的,不能用它來創建同步請求。請求返回之后,會觸發 load 事件,響應的數據也會保存在 responseText 屬性中,如下所示。
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);
??在接收到響應后,你只能訪問響應的原始文本;沒有辦法確定響應的狀態代碼。而且,只要響應有效就會觸發 load 事件,如果失敗(包括響應中缺少 Access-Control-Allow-Origin 頭部)就會觸發 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);
??鑒于導致 XDR 請求失敗的因素很多,因此建議你不要忘記通過 onerror 事件處理程序來捕獲該事件;否則,即使請求失敗也不會有任何提示。
??在請求返回前調用 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 秒鐘后超時,并隨即調用 ontimeout 事件處理程序。
??為支持 POST 請求,XDR 對象提供了 contentType 屬性,用來表示發送數據的格式,如下面的例子所示。
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 的實現
??Firefox 3.5+、Safari 4+、Chrome、iOS 版 Safari 和 Android 平臺中的 WebKit 都通過 XMLHttpRequest 對象實現了對 CORS 的原生支持。在嘗試打開不同來源的資源時,無需額外編寫代碼就可以觸發這個行為。
??要請求位于另一個域中的資源,使用標準的 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()設置自定義頭部。
- 不能發送和接收 cookie。
- 調用 getAllResponseHeaders() 方法總會返回空字符串。
??由于無論同源請求還是跨源請求都使用相同的接口,因此對于本地資源,最好使用相對 URL,在訪問遠程資源時再使用絕對 URL。這樣做能消除歧義,避免出現限制訪問頭部或本地 cookie 信息等問題。
4.3、Preflighted Requests
??CORS 通過一種叫做 Preflighted Requests 的透明服務器驗證機制支持開發人員使用自定義的頭部、GET 或 POST 之外的方法,以及不同類型的主體內容。在使用下列高級選項來發送請求時,就會向服務器發送一個 Preflight 請求。這種請求使用 OPTIONS 方法,發送下列頭部。
- Origin:與簡單的請求相同。
- Access-Control-Request-Method:請求自身使用的方法。
- Access-Control-Request-Headers:(可選)自定義的頭部信息,多個頭部以逗號分隔。
??以下是一個帶有自定義頭部 NCZ 的使用 POST 方法發送的請求。
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
??發送這個請求后,服務器可以決定是否允許這種類型的請求。服務器通過在響應中發送如下頭部與瀏覽器進行溝通。
- Access-Control-Allow-Origin:與簡單的請求相同。
- Access-Control-Allow-Methods:允許的方法,多個方法以逗號分隔。
- Access-Control-Allow-Headers:允許的頭部,多個頭部以逗號分隔。
- Access-Control-Max-Age:應該將這個 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 請求結束后,結果將按照響應中指定的時間緩存起來。而為此付出的代價只是第一次發送這種請求時會多一次 HTTP 請求。
??支持 Preflight 請求的瀏覽器包括 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不支持。
4.4、帶憑據的請求
??默認情況下,跨源請求不提供憑據(cookie、HTTP 認證及客戶端 SSL 證明 等 )。 通 過 將 withCredentials 屬性設置為 true,可以指定某個請求應該發送憑據。如果服務器接受帶憑據的請求,會用下面的 HTTP 頭部來響應。
Access-Control-Allow-Credentials: true
??如果發送的是帶憑據的請求,但服務器的響應中沒有包含這個頭部,那么瀏覽器就不會把響應交給 JavaScript(于是,responseText 中將是空字符串,status 的值為 0,而且會調用 onerror() 事件處理程序)。
??另外,服務器還可以在 Preflight 響應中發送這個 HTTP 頭部,表示允許源發送帶憑據的請求。
??支持 withCredentials 屬性的瀏覽器有 Firefox 3.5+、Safari 4+和 Chrome。IE 10 及更早版本都不支持。
4.5、跨瀏覽器的 CROS
??即使瀏覽器對 CORS 的支持程度并不都一樣,但所有瀏覽器都支持簡單的(非 Preflight 和不帶憑據的)請求,因此有必要實現一個跨瀏覽器的方案。
??檢測 XHR 是否支持 CORS 的最簡單方式,就是檢查是否存在 withCredentials 屬性。再結合檢測 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 對象類似,都提供了夠用的接口,因此以上模式還是相當有用的。這兩個對象共同的屬性/方法如下。
- abort():用于停止正在進行的請求。
- onerror:用于替代 onreadystatechange 檢測錯誤。
- onload:用于替代 onreadystatechange 檢測成功。
- responseText:用于取得響應內容。
- send():用于發送請求。
??以上成員都包含在 createCORSRequest() 函數返回的對象中,在所有瀏覽器中都能正常使用。
5、其他跨域技術
??在 CORS 出現以前,要實現跨域 Ajax 通信頗費一些周折。開發人員想出了一些辦法,利用 DOM 中能夠執行跨域請求的功能,在不依賴 XHR 對象的情況下也能發送某種請求。
??雖然 CORS 技術已經無處不在,但開發人員自己發明的這些技術仍然被廣泛使用,畢竟這樣不需要修改服務器端代碼。
5.1、圖像 Ping
??上述第一種跨域請求技術是使用<img>標簽。我們知道,一個網頁可以從任何網頁中加載圖像,不用擔心跨域不跨域。這也是在線廣告跟蹤瀏覽量的主要方式。
??可以動態地創建圖像,使用它們的 onload 和 onerror 事件處理程序來確定是否接收到了響應。
??動態創建圖像經常用于圖像 Ping。圖像 Ping 是與服務器進行簡單、單向的跨域通信的一種方式。請求的數據是通過查詢字符串形式發送的,而響應可以是任意內容,但通常是像素圖或 204 響應。
??通過圖像 Ping,瀏覽器得不到任何具體的數據,但通過偵聽 load 和 error 事件,它能知道響應是什么時候接收到的。來看下面的例子。
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";
??這里創建了一個 Image 的實例,然后將 onload 和 onerror 事件處理程序指定為同一個函數。這樣無論是什么響應,只要請求完成,就能得到通知。請求從設置 src 屬性那一刻開始,而這個例子在請求中發送了一個 name 參數。
??圖像 Ping 最常用于跟蹤用戶點擊頁面或動態廣告曝光次數。圖像 Ping 有兩個主要的缺點,一是只能發送 GET 請求,二是無法訪問服務器的響應文本。因此,圖像 Ping 只能用于瀏覽器與服務器間的單
向通信。
5.2、JSONP
??JSONP 是 JSON with padding(填充式 JSON 或參數式 JSON)的簡寫,是應用 JSON 的一種新方法,在后來的 Web 服務中非常流行。
??JSONP 看起來與 JSON 差不多,只不過是被包含在函數調用中的 JSON,就像下面這樣。
callback({ "name": "Nicholas" });
??JSONP 由兩部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。
??回調函數的名字一般是在請求中指定的。而數據就是傳入回調函數中的 JSON 數據。下面是一個典型的 JSONP 請求。
http://freegeoip.net/json/?callback=handleResponse
??這個 URL 是在請求一個 JSONP 地理定位服務。通過查詢字符串來指定 JSONP 服務的回調參數是很常見的,就像上面的 URL 所示,這里指定的回調函數的名字叫 handleResponse()。
??JSONP 是通過動態<script>元素來使用的,使用時可以為 src 屬性指定一個跨域 URL。
??這里的<script>元素與<img>元素類似,都有能力不受限制地從其他域加載資源。因為 JSONP 是有效的 JavaScript 代碼,所以在請求完成后,即在 JSONP 響應加載到頁面中以后,就會立即執行。來看一個例子。
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);
??上述例子通過查詢地理定位服務來顯示你的 IP 地址和位置信息。
??JSONP 之所以在開發人員中極為流行,主要原因是它非常簡單易用。與圖像 Ping 相比,它的優點在于能夠直接訪問響應文本,支持在瀏覽器與服務器之間雙向通信。
??不過,JSONP 也有兩點不足。
??首先,JSONP 是從其他域中加載代碼執行。如果其他域不安全,很可能會在響應中夾帶一些惡意代碼,而此時除了完全放棄 JSONP 調用之外,沒有辦法追究。因此在使用不是你自己運維的 Web 服務時,一定得保證它安全可靠。
??其次,要確定 JSONP 請求是否失敗并不容易。雖然 HTML5 給<script>元素新增了一個 onerror 事件處理程序,但目前還沒有得到任何瀏覽器支持。為此,開發人員不得不使用計時器檢測指定時間內是否接收到了響應。但就算這樣也不能盡如人意,畢竟不是每個用戶上網的速度和帶寬都一樣。
5.3、Comet
??Comet 是 Alex Russell( Alex Russell 是著名 JavaScript 框架 Dojo 的創始人。)發明的一個詞兒,指的是一種更高級的 Ajax 技術(經常也有人稱為“服務器推送”)。
??Ajax 是一種從頁面向服務器請求數據的技術,而 Comet 則是一種服務器向頁面推送數據的技術。
??Comet 能夠讓信息近乎實時地被推送到頁面上,非常適合處理體育比賽的分數和股票報價。
??有兩種實現 Comet 的方式:長輪詢 和 流。
??長輪詢是傳統輪詢(也稱為短輪詢)的一個翻版,即瀏覽器定時向服務器發送請求,看有沒有更新的數據。下圖展示的是短輪詢的時間線。
??長輪詢把短輪詢顛倒了一下。頁面發起一個到服務器的請求,然后服務器一直保持連接打開,直到有數據可發送。發送完數據之后,瀏覽器關閉連接,隨即又發起一個到服務器的新請求。這一過程在頁
面打開期間一直持續不斷。下圖展示了長輪詢的時間線。
??無論是短輪詢還是長輪詢,瀏覽器都要在接收數據之前,先發起對服務器的連接。兩者最大的區別在于服務器如何發送數據。短輪詢是服務器立即發送響應,無論數據是否有效,而長輪詢是等待發送響應。
??輪詢的優勢是所有瀏覽器都支持,因為使用 XHR 對象和 setTimeout() 就能實現。而你要做的就是決定什么時候發送請求。
??第二種流行的 Comet 實現是** HTTP 流**。流不同于上述兩種輪詢,因為它在頁面的整個生命周期內只使用一個 HTTP 連接。具體來說,就是瀏覽器向服務器發送一個請求,而服務器保持連接打開,然后周期性地向瀏覽器發送數據。比如,下面這段 PHP 腳本就是采用流實現的服務器中常見的形式。
<?php
$i = 0;
while(true){
// 輸出一些數據,然后立即刷新輸出緩存
echo "Number is $i";
flush();
// 等幾秒鐘
sleep(10);
$i++;
}
??所有服務器端語言都支持打印到輸出緩存然后刷新(將輸出緩存中的內容一次性全部發送到客戶端)的功能。而這正是實現 HTTP 流的關鍵所在。
??在 Firefox、Safari、Opera 和 Chrome 中,通過偵聽 readystatechange 事件及檢測 readyState 的值是否為 3,就可以利用 XHR 對象實現 HTTP 流。
??在上述這些瀏覽器中,隨著不斷從服務器接收數據,readyState 的值會周期性地變為 3。當 readyState 值變為 3 時,responseText 屬性中就會保存接收到的所有數據。此時,就需要比較此前接收到的數據,決定從什么位置開始取得最新的數據。使用 XHR 對象實現 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){
// 只取得最新數據并調整計數器
result = xhr.responseText.substring(received);
received += result.length;
// 調用 progress 回調函數
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() 函數接收三個參數:要連接的 URL、在接收到數據時調用的函數以及關閉連接時調用的函數。
??有時候,當連接關閉時,很可能還需要重新建立,所以關注連接什么時候關閉還是有必要的。
??只要 readystatechange 事件發生,而且 readyState 值為 3,就對 responseText 進行分割以取得最新數據。這里的 received 變量用于記錄已經處理了多少個字符,每次 readyState 值為 3 時都遞增。
??然后,通過 progress 回調函數來處理傳入的新數據。而當 readyState 值為 4 時,則執行 finished 回調函數,傳入響應返回的全部內容。
??雖然這個例子比較簡單,而且也能在大多數瀏覽器中正常運行(IE 除外),但管理 Comet 的連接是很容易出錯的,需要時間不斷改進才能達到完美。瀏覽器社區認為 Comet 是未來 Web 的一個重要組成
部分,為了簡化這一技術,又為 Comet 創建了兩個新的接口。
5.4、服務器發送事件
??SSE(Server-Sent Events,服務器發送事件)是圍繞只讀 Comet 交互推出的 API 或者模式。
??SSE API 用于創建到服務器的單向連接,服務器通過這個連接可以發送任意數量的數據。
??服務器響應的 MIME 類型必須是 text/event-stream,而且是瀏覽器中的 JavaScript API 能解析格式輸出。
??SSE 支持短輪詢、長輪詢和 HTTP 流,而且能在斷開連接時自動確定何時重新連接。有了這么簡單實用的 API,再實現 Comet 就容易多了。
??支持 SSE 的瀏覽器有 Firefox 6+、Safari 5+、Opera 11+、Chrome 和 iOS 4+版 Safari。
1. SSE API
??SSE 的 JavaScript API 與其他傳遞消息的 JavaScript API 很相似。要預訂新的事件流,首先要創建一個新的 EventSource 對象,并傳進一個入口點:
var source = new EventSource("myevents.php");
??注意,傳入的 URL 必須與創建對象的頁面同源(相同的 URL 模式、域及端口)。EventSource 的實例有一個 readyState 屬性,值為 0 表示正連接到服務器,值為 1 表示打開了連接,值為 2 表示關閉
了連接。另外,還有以下三個事件。
- open:在建立連接時觸發。
- message:在從服務器接收到新事件時觸發。
- error:在無法建立連接時觸發。
??就一般的用法而言,onmessage 事件處理程序也沒有什么特別的。
source.onmessage = function(event){
var data = event.data;
// 處理數據
};
??服務器發回的數據以字符串形式保存在 event.data 中。
??默認情況下,EventSource 對象會保持與服務器的活動連接。如果連接斷開,還會重新連接。這就意味著 SSE 適合長輪詢和 HTTP 流。如果想強制立即斷開連接并且不再重新連接,可以調用 close() 方法。
source.close();
2. 事件流
??所謂的服務器事件會通過一個持久的 HTTP 響應發送,這個響應的 MIME 類型為 text/event-stream。響應的格式是純文本,最簡單的情況是每個數據項都帶有前綴 data:,例如:
data: foo
data: bar
data: foo
data: bar
??對以上響應而言,事件流中的第一個 message 事件返回的 event.data 值為"foo",第二個 message 事件返回的 event.data 值為"bar",第三個 message 事件返回的 event.data 值為 "foo\nbar"(注意中間的換行符)。
??對于多個連續的以 data:開頭的數據行,將作為多段數據解析,每個值之間以一個換行符分隔。只有在包含 data:的數據行后面有空行時,才會觸發 message 事件,因此在服務器上生成事件流時不能忘了多添加這一行。
??通過 id:前綴可以給特定的事件指定一個關聯的 ID,這個 ID 行位于 data:行前面或后面皆可:
data: foo
id: 1
??設置了 ID 后,EventSource 對象會跟蹤上一次觸發的事件。如果連接斷開,會向服務器發送一個包含名為 Last-Event-ID 的特殊 HTTP 頭部的請求,以便服務器知道下一次該觸發哪個事件。在多次連接的事件流中,這種機制可以確保瀏覽器以正確的順序收到連接的數據段。
5.5、Web Sockets
??要說最令人津津樂道的新瀏覽器 API,就得數 Web Sockets 了。Web Sockets 的目標是在一個單獨的持久連接上提供全雙工、雙向通信。
??在 JavaScript 中創建了 Web Socket 之后,會有一個 HTTP 請求發送到瀏覽器以發起連接。在取得服務器響應后,建立的連接會使用 HTTP 升級從 HTTP 協議交換為 Web Socket 協議。
??也就是說,使用標準的 HTTP 服務器無法實現 Web Sockets,只有支持這種協議的專門服務器才能正常工作。
??由于 Web Sockets 使用了自定義的協議,所以 URL 模式也略有不同。未加密的連接不再是 http://,而是 ws://;加密的連接也不是 https://,而是 wss://。
??在使用 Web Socket URL 時,必須帶著這個模式,因為將來還有可能支持其他模式。
??使用自定義協議而非 HTTP 協議的好處是,能夠在客戶端和服務器之間發送非常少量的數據,而不必擔心 HTTP 那樣字節級的開銷。由于傳遞的數據包很小,因此 Web Sockets 非常適合移動應用。畢竟
對移動應用而言,帶寬和網絡延遲都是關鍵問題。
??使用自定義協議的缺點在于,制定協議的時間比制定 JavaScript API 的時間還要長。
??Web Sockets 曾幾度擱淺,就因為不斷有人發現這個新協議存在一致性和安全性的問題。Firefox 4 和 Opera 11 都曾默認啟用 Web Sockets,但在發布前夕又禁用了,因為又發現了安全隱患。目前支持 Web Sockets 的瀏覽器有 Firefox 6+、Safari 5+、Chrome 和 iOS 4+版 Safari。
1. Web Sockets API
??要創建 Web Socket,先實例一個 WebSocket 對象并傳入要連接的 URL:
var socket = new WebSocket("ws://www.example.com/server.php");
??注意,必須給 WebSocket 構造函數傳入絕對 URL。同源策略對 Web Sockets 不適用,因此可以通過它打開到任何站點的連接。至于是否會與某個域中的頁面通信,則完全取決于服務器。(通過握手信
息就可以知道請求來自何方。)
??實例化了 WebSocket 對象后,瀏覽器就會馬上嘗試創建連接。與 XHR 類似,WebSocket 也有一個表示當前狀態的 readyState 屬性。不過,這個屬性的值與 XHR 并不相同,而是如下所示。
- WebSocket.OPENING (0):正在建立連接。
- WebSocket.OPEN (1):已經建立連接。
- WebSocket.CLOSING (2):正在關閉連接。
- WebSocket.CLOSE (3):已經關閉連接。
??WebSocket 沒有 readystatechange 事件;不過,它有其他事件,對應著不同的狀態。readyState 的值永遠從 0 開始。要關閉 Web Socket 連接,可以在任何時候調用 close() 方法。
socket.close();
??調用了 close() 之后,readyState 的值立即變為 2(正在關閉),而在關閉連接后就會變成 3。
2. 發送和接收數據
??Web Socket 打開之后,就可以通過連接發送和接收數據。要向服務器發送數據,使用 send() 方法并傳入任意字符串,例如:
var socket = new WebSocket("ws://www.example.com/server.php");
socket.send("Hello world!");
??因為 Web Sockets 只能通過連接發送純文本數據,所以對于復雜的數據結構,在通過連接發送之前,必須進行序列化。下面的例子展示了先將數據序列化為一個 JSON 字符串,然后再發送到服務器:
var message = {
time: new Date(),
text: "Hello world!",
clientId: "asdfp8734rew"
};
socket.send(JSON.stringify(message));
??接下來,服務器要讀取其中的數據,就要解析接收到的 JSON 字符串。
??當服務器向客戶端發來消息時,WebSocket 對象就會觸發 message 事件。這個 message 事件與其他傳遞消息的協議類似,也是把返回的數據保存在 event.data 屬性中。
socket.onmessage = function(event){
var data = event.data;
// 處理數據
};
??與通過 send() 發送到服務器的數據一樣,event.data 中返回的數據也是字符串。如果你想得到其他格式的數據,必須手工解析這些數據。
3. 其他事件
??WebSocket 對象還有其他三個事件,在連接生命周期的不同階段觸發。
- open:在成功建立連接時觸發。
- error:在發生錯誤時觸發,連接不能持續。
- close:在連接關閉時觸發。
??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 是一個布爾值,表示連接是否已經明確地關
閉;code 是服務器返回的數值狀態碼;而 reason 是一個字符串,包含服務器發回的消息。可以把這些信息顯示給用戶,也可以記錄到日志中以便將來分析。
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 服務器?因為 Web Socket 協議不同于 HTTP,所以現有服務器不能用于 Web Socket 通信。SSE 倒是通過常規 HTTP 通信,因此現有服務器就可以滿足需求。
??第二個要考慮的問題是到底需不需要雙向通信。如果用例只需讀取服務器數據(如比賽成績),那么 SSE 比較容易實現。如果用例必須雙向通信(如聊天室),那么 Web Sockets 顯然更好。
??別忘了,在不能選擇 Web Sockets 的情況下,組合 XHR 和 SSE 也是能實現雙向通信的。
6、安全
??討論 Ajax 和 Comet 安全的文章可謂連篇累牘,而相關主題的書也已經出了很多本了。大型 Ajax 應用程序的安全問題涉及面非常之廣,但我們可以從普遍意義上探討一些基本的問題。
??首先,可以通過 XHR 訪問的任何 URL 也可以通過瀏覽器或服務器來訪問。下面的 URL 就是一個例子。
/getuserinfo.php?id=23
??如果是向這個 URL 發送請求,可以想象結果會返回 ID 為 23 的用戶的某些數據。誰也無法保證別人不會將這個 URL 的用戶 ID 修改為 24、56 或其他值。因此,getuserinfo.php 文件必須知道請求者是否真的有權限訪問要請求的數據;否則,你的服務器就會門戶大開,任何人的數據都可能被泄漏出去。
??對于未被授權系統有權訪問某個資源的情況,我們稱之為 CSRF(Cross-Site Request Forgery,跨站點請求偽造)。未被授權系統會偽裝自己,讓處理請求的服務器認為它是合法的。
??受到 CSRF 攻擊的 Ajax 程序有大有小,攻擊行為既有旨在揭示系統漏洞的惡作劇,也有惡意的數據竊取或數據銷毀。
??為確保通過 XHR 訪問的 URL 安全,通行的做法就是驗證發送請求者是否有權限訪問相應的資源。有下列幾種方式可供選擇。
- 要求以 SSL 連接來訪問可以通過 XHR 請求的資源。
- 要求每一次請求都要附帶經過相應算法計算得到的驗證碼。
??請注意,下列措施對防范 CSRF 攻擊不起作用。
- 要求發送 POST 而不是 GET 請求——很容易改變。
- 檢查來源 URL 以確定是否可信——來源記錄很容易偽造。
- 基于 cookie 信息進行驗證——同樣很容易偽造。
??XHR 對象也提供了一些安全機制,雖然表面上看可以保證安全,但實際上卻相當不可靠。
??實際上,前面介紹的 open() 方法還能再接收兩個參數:要隨請求一起發送的用戶名和密碼。帶有這兩個參數的請求可以通過 SSL 發送給服務器上的頁面,如下面的例子所示。
xhr.open("get", "example.php", true, "username", "password"); // 不要這樣做!!
??即便可以考慮這種安全機制,但還是盡量不要這樣做。把用戶名和密碼保存在 JavaScript 代碼中本身就是極為不安全的。任何人,只要他會使用 JavaScript 調試器,就可以通過查看相應的變量發現純文本形式的用戶名和密碼。
小結
??Ajax 是無需刷新頁面就能夠從服務器取得數據的一種方法。關于 Ajax,可以從以下幾方面來總結一下。
- 負責 Ajax 運作的核心對象是 XMLHttpRequest(XHR)對象。
- XHR 對象由微軟最早在 IE5 中引入,用于通過 JavaScript 從服務器取得 XML 數據。
- 在此之后,Firefox、Safari、Chrome 和 Opera 都實現了相同的特性,使 XHR 成為了 Web 的一個事實標準。
- 雖然實現之間存在差異,但 XHR 對象的基本用法在不同瀏覽器間還是相對規范的,因此可以放心地用在 Web 開發當中。
??同源策略是對 XHR 的一個主要約束,它為通信設置了“相同的域、相同的端口、相同的協議”這一限制。試圖訪問上述限制之外的資源,都會引發安全錯誤,除非采用被認可的跨域解決方案。
??這個解決方案叫做 CORS(Cross-Origin Resource Sharing,跨源資源共享),IE8 通過 XDomainRequest 對象支持 CORS,其他瀏覽器通過 XHR 對象原生支持 CORS。圖像 Ping 和 JSONP 是另外兩種跨域通信的技術,但不如 CORS 穩妥。
??Comet 是對 Ajax 的進一步擴展,讓服務器幾乎能夠實時地向客戶端推送數據。實現 Comet 的手段主要有兩個:長輪詢和 HTTP 流。所有瀏覽器都支持長輪詢,而只有部分瀏覽器原生支持 HTTP 流。
??SSE(Server-Sent Events,服務器發送事件)是一種實現 Comet 交互的瀏覽器 API,既支持長輪詢,也支持 HTTP 流。
??Web Sockets 是一種與服務器進行全雙工、雙向通信的信道。與其他方案不同,Web Sockets 不使用 HTTP 協議,而使用一種自定義的協議。這種協議專門為快速傳輸小數據設計。雖然要求使用不同的 Web 服務器,但卻具有速度上的優勢。
??各方面對 Ajax 和 Comet 的鼓吹吸引了越來越多的開發人員學習 JavaScript,人們對 Web 開發的關注也再度升溫。與 Ajax 有關的概念都還相對比較新,這些概念會隨著時間推移繼續發展。