第17章 Ajax 與 JSON (一)

 

2005年,Jesse James Garrett 發表了一篇在線文章,題爲 "Ajax: A new Approach to Web Applications"。他在這篇文章裏介紹了一種技術,用他的話說,就叫Ajax, 是對 Asynchronous JavaScript + XML 的簡寫。這一技術能夠向服務器請求額外的數據而無須卸載頁面,會帶來更好的用戶體驗。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 開發人員必須掌握的一種技能。

17.1 XHR 對象

var xmlHttp;

function createXMLHttpRequest(){

if (window.ActiveXObject) {

xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");

} else if (window.XMLHttpRequest){

xmlHttp = new XMLHttpRequest();

}

}

IE5 是第一款引入 XHR 對象的瀏覽器。在 IE5 中,XHR 對象是通過 MSXML 庫中的一個 ActiveX 對象實現的。因此,在 IE 中可能會遇到 3 種不同版本的 XHR 對象,即:MSXML2.XMLHttp、MSXML2.XMLHttp.3.0 和 MSXML2.XMLHttp.6.0 。

IE7、Firefox、Opera、Chrome 和 Safari 都支持原生的 XHR 對象,在這些瀏覽器中創建 XHR 對象要像下面這樣使用 XMLHttpRequest 構造函數:

var xhr = new XMLHttpRequest();

假如你只想支持 IE7 及更高版本,那麼可以只用原生的 XHR 實現。

17.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.php", 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.statusText);
} 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,而 Safari 3 之前的版本則會將 status 設置爲 undefined 。
像前面這樣發送同步請求當然沒有問題,但多數情況下,我們還是要發送異步請求,才能讓 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", "exmple.txt", true);
xhr.send(null);
以上代碼利用 DOM0級方法爲 XHR 對象添加了事件處理程序,原因是並非所有瀏覽器都支持 DOM2級方法。與其他事件處理程序不同,這裏沒有向 onreadystatechange 事件處理程序中傳遞 event 對象;必須通過 XHR 對象本身來確定下一步該怎麼做。
這個例子在 onreadystatechange 事件處理程序中使用了 xhr 對象,沒有使用 this 對象,原因是 onreadystatechange 事件處理程序的作用域問題。如果使用 this 對象,在有的瀏覽器中會導致函數執行失敗,或者導致錯誤發生。因此,使用實際的 XHR 對象實例變量是較爲可靠的一種方式。
另外,在接收到響應之前還可以調用 abort() 方法來取消異步請求,如下所示:
xhr.abort();
調用這個方法後,XHR 對象會停止觸發事件,而且也不再允許訪問任何與響應有關的對象屬性。在終止請求之後,還應該對 XHR 對象進行解引用操作。由於內存原因,不建議重用 XHR 對象。

17.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.txt", 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: clos
Content-Type: text/html; charset=iso-8859-1
這種格式化的輸出可以方便我們檢查響應中所有頭部字段的名稱,而不必一個一個地檢查某個字段是否存在。

17.1.3 GET請求

GET 是最常見的請求類型,最常用於向服務器查詢某些信息。必要時,可以將查詢字符串參數追加到 URL 的末尾,以便將信息發送給服務器。對 XHR 而言,位於傳入 open() 方法的 URL 末尾的查詢字符串必須經過正確的編碼才行。
使用 GET 請求經常會發生的一個錯誤,就是查詢字符串的格式有問題。查詢字符串中每個參數的名稱和值都必須使用 encodeURIComponent() 進行編碼,然後才能放到 URL 的末尾;而且所有名值對都必須由和號 (&) 分隔,如下面的例子所示:
xhr.open("get", "example.php?name=1=value& name2=value2", true);
下面這個函數可以輔助向現有 URL 的末尾添加查詢字符串參數:
function addURLParam(url, name, value){
url += url.indexOf("?") == -1 ? "?" : " & ";
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
這個 addURLParam() 函數接受3個參數:要添加參數的URL、參數的名稱和參數的值。這個函數首先檢查 URL 是否包含問號 (以確定是否已經有參數存在)。如果沒有,就添加一個問號;否則,就添加一個和號。然後,將參數名稱和值進行編碼,再添加到 URL 的末尾。最後返回添加參數之後的 URL。
下面是使用這個函數來構建請求URL 的示例:
var url = "example.php";
// 添加參數
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Porfessional JavaScript");

// 初始化請求
xhr.open("get", url, false);
在這裏使用 addURLParam() 函數可以確保查詢字符串的格式良好,並可靠地用於 XHR 對象。

17.1.4 POST 請求

使用頻率僅次於 GET 的是 POST 請求,通常用於向服務器發送應該被保存的數據。POST 請求應該把數據作爲請求的主體提交,而 GET 請求傳統上不是這樣。POST 請求的主體可以包含非常多的數據,而且格式不限。在 open() 方法第一個參數的位置傳入 "post" ,就可以初始化一個 POST 請求,如下面的例子所示:
xhr.open("open", "example.php", true);
發送 POST 請求的第二步就是向 send() 方法中傳入某些數據。由於 XHR 最初的設計主要是爲了處理 XML,因此可以在此傳入 XML DOM 文檔,傳入的文檔經序列化之後將作爲請求主體被提交到服務器。當然,也可以在此傳入任何想發送給服務器的字符串。
默認情況下,服務器對 POST 請求和提交 Web 表單的請求並不會一視同仁。因此,服務器端必須有程序來讀取發送過來的原始數據,並從中解析出有用的部分。不過,我們可以使用 XHR 來模仿表單提交:首先將 Content-Type 頭部信息設置爲 application/x-www-form-urlencoded ,也就是表單提交時的內容類型,其次是以適當的格式創建一個字符串。第13章經討論過,POST 數據的格式與查詢字符串格式相同。如果需要將頁面中表單的數據進行序列化,然後再通過 XHR 發送到服務器,那麼就可以使用第 13 章介紹的 serialize() 函數來創建這個字符串:
function submitData(){
	var xhr = createXHR();		
	xhr.onreadystatechange = function(event){
		if(xhr.readystatechange == 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" 的表單中的數據序列化之後發送給服務器。
與 GET 請求相比,POST 請求消耗的資源會更多一些。從性能角度來看,以發送相同的數據計,GET請求的速度最多可達到 POST 請求的兩倍。

17.1.6 安全

首先,可以通過 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 調試器,就可以通過查看相應的變量發現純文本形式的用戶名和密碼。
發佈了0 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章