Ajax技術(shù)的核心是XMLHttpRequest對(duì)象(簡(jiǎn)稱XHR),可以通過(guò)使用XHR對(duì)象獲取到服務(wù)器的數(shù)據(jù),然后再通過(guò)DOM將數(shù)據(jù)插入到頁(yè)面中呈現(xiàn)。雖然名字中包含XML,但Ajax通訊與數(shù)據(jù)格式無(wú)關(guān),所以我們的數(shù)據(jù)格式可以是XML或JSON等格式。
XMLHttpRequest對(duì)象用于在后臺(tái)與服務(wù)器交換數(shù)據(jù),具體作用如下:
XMLHttpRequest是一個(gè)JavaScript對(duì)象,它是由微軟設(shè)計(jì),并且被Mozilla、Apple和Google采納,W3C正在標(biāo)準(zhǔn)化它。它提供了一種簡(jiǎn)單的方法來(lái)檢索URL中的數(shù)據(jù)。
我們要?jiǎng)?chuàng)建一個(gè)XMLHttpRequest實(shí)例,只需new一個(gè)就OK了:
//// Creates a XMLHttpRequest object.var req = new XMLHttpRequest();
也許有人會(huì)說(shuō):“這可不行啊!IE6不支持原始的XHR對(duì)象”,確實(shí)是這樣,我們?cè)诤竺鎸?huì)介紹支持IE6或更老版本創(chuàng)建XHR對(duì)象的方法。
在創(chuàng)建XHR對(duì)象后,接著我們要調(diào)用一個(gè)初始化方法open(),它接受五個(gè)參數(shù)具體定義如下:
void open( DOMString method, //"GET", "POST", "PUT", "DELETE" DOMString url, optional boolean async, optional DOMString user, optional DOMString password);
通過(guò)上面的定義我們知道open()方法的簽名包含五個(gè)參數(shù),其中有參數(shù)method和url地址是必填的,假設(shè)我們針對(duì)URL: myxhrtest.aspx發(fā)送GET請(qǐng)求獲取數(shù)據(jù),具體定義如下:
var req = new XMLHttpRequest();req.open( "GET", "myxhrtest.aspx", false);
通過(guò)上述代碼會(huì)啟動(dòng)一個(gè)針對(duì)myxhrtest.aspx的GET請(qǐng)求,這里有兩點(diǎn)要注意:一是URL相對(duì)于執(zhí)行代碼的當(dāng)前頁(yè)面(使用絕對(duì)路徑);二是調(diào)用open()方法并不會(huì)真正發(fā)送請(qǐng)求,而只是啟動(dòng)一個(gè)請(qǐng)求準(zhǔn)備發(fā)送。
只能向同一個(gè)域中使用相同端口和協(xié)議的URL中發(fā)送請(qǐng)求;如果URL與啟動(dòng)請(qǐng)求的頁(yè)面有任何差別,都會(huì)引發(fā)安全錯(cuò)誤。
要真正發(fā)送請(qǐng)求要使用send()方法,send()方法接受一個(gè)參數(shù),即要作為請(qǐng)求主體發(fā)送的數(shù)據(jù),如果不需要通過(guò)請(qǐng)求主體發(fā)送數(shù)據(jù),我們必須傳遞一個(gè)null值。在調(diào)用send()之后,請(qǐng)求就會(huì)被分派到服務(wù)器,完整Ajax請(qǐng)求代碼如下:
var req = new XMLHttpRequest();req.open( "GET", "myxhrtest.aspx", false);req.send(null);
在發(fā)送請(qǐng)求之后,我們需要檢查請(qǐng)求是否執(zhí)行成功,首先可以通過(guò)status屬性判斷,一般來(lái)說(shuō),可以將HTTP狀態(tài)代碼為200作為成功標(biāo)志。這時(shí),響應(yīng)主體內(nèi)容會(huì)保存到responseText中。此外,狀態(tài)代碼為304表示請(qǐng)求的資源并沒(méi)有被修改,可以直接使用瀏覽器緩存的數(shù)據(jù),Ajax的同步請(qǐng)求代碼如下:
if (req != null) { req.onreadystatechange = function() { if ((req.status >= 200 && req.status < 300) || req.status == 304) { //// Do something. } else { alert("Request was unsuccessful: " + req.status); } }; req.open("GET", "www.myxhrtest.aspx", true); req.send(null);}
前面我們定義了Ajax的同步請(qǐng)求,如果我們發(fā)送異步請(qǐng)求,那么在請(qǐng)求過(guò)程中javascript代碼會(huì)繼續(xù)執(zhí)行,這時(shí)可以通過(guò)readyState屬性判斷請(qǐng)求的狀態(tài),當(dāng)readyState = 4時(shí),表示收到全部響應(yīng)數(shù)據(jù),屬性值的定義如下:
readyState值 | 描述 |
0 | 未初始化;尚未調(diào)用open()方法 |
1 | 啟動(dòng);尚未調(diào)用send()方法 |
2 | 已發(fā)送;但尚未收到響應(yīng) |
3 | 接收;已經(jīng)收到部分響應(yīng)數(shù)據(jù) |
4 | 完成;收到全部響應(yīng)數(shù)據(jù) |
表1 readyState屬性值
同步請(qǐng)求:發(fā)生請(qǐng)求后,要等待服務(wù)器執(zhí)行完畢才繼續(xù)執(zhí)行當(dāng)前代碼。
異步請(qǐng)求:發(fā)生請(qǐng)求后,無(wú)需等到服務(wù)器執(zhí)行完畢,可以繼續(xù)執(zhí)行當(dāng)前代碼。
現(xiàn)在我們要增加判斷readyState屬性值,當(dāng)readyState = 4時(shí),表示全部數(shù)據(jù)接收完成, 所以Ajax的異步請(qǐng)求代碼如下:
if (req != null) { req.onreadystatechange = function() { //// Checks the asyn request completed or not. if (req.readyState == 4) { if ((req.status >= 200 && req.status < 300) || req.status == 304) { //// Do something. } else { alert("Request was unsuccessful: " + req.status); } } }; req.open("GET", "www.myxhrtest.aspx", true); req.send(null);}
現(xiàn)在我們對(duì)Ajax的請(qǐng)求實(shí)現(xiàn)有了初步的了解,接下來(lái)我們將通過(guò)具體的例子說(shuō)明Ajax請(qǐng)求的應(yīng)用場(chǎng)合和局限。
在日常網(wǎng)絡(luò)生活中,我們?cè)跒g覽器的地址中輸入要訪問(wèn)的URL并且回車,瀏覽器會(huì)向服務(wù)器發(fā)送請(qǐng)求,當(dāng)服務(wù)器收到請(qǐng)求后,把相應(yīng)的請(qǐng)求頁(yè)面發(fā)送回瀏覽器,我們會(huì)發(fā)現(xiàn)頁(yè)面大部分加載完畢,有些還沒(méi)有加載完畢??偟脕?lái)說(shuō),采用異步加載方式不會(huì)影響已加載完畢的頁(yè)面瀏覽,我們可以通過(guò)Ajax實(shí)現(xiàn)異步加載。
這里我們以AdventureWorks數(shù)據(jù)庫(kù)為例,把產(chǎn)品表(Product)中的數(shù)據(jù)通過(guò)報(bào)表呈現(xiàn)給用戶,我們可以通過(guò)多種方法實(shí)現(xiàn)該報(bào)表需求,這里我們將通過(guò)Ajax實(shí)現(xiàn)該功能。
首先,我們要把后臺(tái)數(shù)據(jù)轉(zhuǎn)換為JSON格式,接下來(lái)我們定義Product表的數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象(DAO),具體的實(shí)現(xiàn)代碼如下:
////// The product datatable dao./// public class ProductDao{ ////// Initializes a new instance of the public ProductDao() { } ///class. /// /// Gets or sets the product id. /// public int Id { get; set; } ////// Gets or sets the product name. /// public string Name { get; set; } ////// Gets or sets the product serial number. /// public string SerialNumber { get; set; } ////// Gets or sets the product qty. /// public short Qty { get; set; }}
前面我們定義了Product表的數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象——ProductDao,它包含四個(gè)屬性分別是產(chǎn)品的Id,名稱,序列號(hào)和銷售數(shù)量。
接下來(lái),讓我們實(shí)現(xiàn)Product表的數(shù)據(jù)庫(kù)操作類。
////// Product table data access manager./// public class ProductManager{ ////// The query sql. /// private const string Query = "SELECT ProductID, Name, ProductNumber, SafetyStockLevel FROM Production.Product"; ////// Stores the object of private IList<ProductDao> _products = new List<ProductDao>(); ///into list. /// /// Gets all products in product table. /// ////// The list ofobject. /// public IList<ProductDao> GetAllProducts() { using (var con = new SqlConnection(ConfigurationManager.ConnectionStrings["SQLCONN"].ToString())) using (var com = new SqlCommand(Query, con)) { con.Open(); using (var reader = com.ExecuteReader(CommandBehavior.CloseConnection)) { while (reader.Read()) { var product = new ProductDao { Id = (int)reader["ProductID"], Name = (string)reader["Name"], SerialNumber = (string)reader["ProductNumber"], Qty = (short)reader["SafetyStockLevel"] }; _products.Add(product); } } } return _products; }}
前面我們實(shí)現(xiàn)了Product表的數(shù)據(jù)庫(kù)操作類——ProductManager,它包含兩個(gè)私有字段Quey和_products,還有一個(gè)獲取Product表中數(shù)據(jù)的方法——GetAllProducts()。
通過(guò)實(shí)現(xiàn)ProductDao和ProductManager,而且我們提供GetAllProducts()方法,獲取Product表中的數(shù)據(jù),接下來(lái)我們要調(diào)用該方法獲取數(shù)據(jù)。
為了使數(shù)據(jù)通過(guò)JSON格式傳遞給頁(yè)面,這里我們要?jiǎng)?chuàng)建一般處理程序(ASHX文件),
一般處理程序適用場(chǎng)合:
圖1一般處理程序
把一般處理程序文件添加到項(xiàng)目中時(shí),會(huì)添加一個(gè)擴(kuò)展名為.ashx的文件,現(xiàn)在我們創(chuàng)建一個(gè)一般處理程序ProductInfo,具體代碼如下:
<%@ WebHandler Language="C#" Class="ProductInfo" %>using System.Runtime.Serialization.Json;using System.Web;using ASP.App_Code;////// The product data handler./// public class ProductInfo : IHttpHandler { public void ProcessRequest (HttpContext context) { context.Response.ContentType = "application/json"; // Creates aoject. var manager = new ProductManager(); // Invokes the GetAllProducts method. var products = manager.GetAllProducts(); // Serializes data to json format. var json = new DataContractJsonSerializer(products.GetType()); json.WriteObject(context.Response.OutputStream, products); } // Whether can resuable by other handler or not. public bool IsReusable { get { return false; } }}
大家注意到ProductInfo類實(shí)現(xiàn)了IHttpHandler接口,該接口包含一個(gè)方法ProcessRequest()方法和一個(gè)屬性IsReusable。ProcessRequest()方法用于處理入站的Http請(qǐng)求。在默認(rèn)情況下,ProductInfo類會(huì)把內(nèi)容類型改為application/json,然后我們把數(shù)據(jù)通過(guò)JSON格式寫入輸入流中;IsReusable屬性表示相同的處理程序是否可以用于多個(gè)請(qǐng)求,這里我們?cè)O(shè)置為false,如果為了提高性能也可以設(shè)置為true。
如下圖所示,我們通過(guò)ProductInfo類成功地實(shí)現(xiàn)獲取數(shù)據(jù)到響應(yīng)流中,并且以JSON格式顯示出來(lái)。
圖2 Http請(qǐng)求
當(dāng)我們請(qǐng)求ProductInfo時(shí), 首先它會(huì)調(diào)用ProcessRequest()方法,接著調(diào)用GetAllProducts()方法從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),然后把數(shù)據(jù)通過(guò)JSON格式寫入到響應(yīng)流中。
現(xiàn)在,我們已經(jīng)成功地把數(shù)據(jù)通過(guò)JSON格式寫入到響應(yīng)流當(dāng)中,接著我們將通過(guò)Ajax方式請(qǐng)求數(shù)據(jù)并且把數(shù)據(jù)顯示到頁(yè)面中。
首先,我們定義方法createXHR()用來(lái)創(chuàng)建XMLHttpRequest對(duì)象,前面我們提到IE6或者更老的版本不支持XMLHttpRequest()方法來(lái)創(chuàng)建XMLHttpRequest對(duì)象,所以我們要在createXHR()方法中,增加判斷當(dāng)前瀏覽器是否IE6或更老的版本,如果是,就要通過(guò)MSXML庫(kù)的一個(gè)ActiveX對(duì)象實(shí)現(xiàn)。因此,在IE中可能遇到三種不同版本的XHR對(duì)象(MSXML2.XMLHttp6.0,MSXML2.XMLHttp3.0和MSXML2.XMLHttp)。
// Creates a XMLHttpRequest object bases on web broswer.function createXHR() { // Checks whether support XMLHttpRequest or not. if (typeof XMLHttpRequest != "undefined") { return new XMLHttpRequest(); } // IE6 and elder version. else if (typeof ActiveXObject != "undefined") { if (typeof arguments.callee.activeXString != "string") { var versions = [ "MSXML2.XMLHttp6.0", "MSXML2.XMLHttp3.0", "MSXML2.XMLHttp"]; for (var i = 0; i < versions.length; i++) { try { var xhr = new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; return xhr; } catch (ex) { throw new Error(ex.toString()); } } return new ActiveXObject(arguments.callee.activeXString); } else { throw new Error("No XHR object available"); } } return null;}$(document).ready(function() { GetDataFromServer();});
前面我們定義了一個(gè)比較通用的方法用來(lái)創(chuàng)建XMLHttpRequest對(duì)象,并且它支持IE6或更老版本創(chuàng)建XMLHttpRequest對(duì)象,接下來(lái)我們將通過(guò)Ajax方法請(qǐng)求數(shù)據(jù)。
function GetDataFromServer() { // Creates a XMLHttpRequest object. var req = new createXHR(); if (req != null) { req.onreadystatechange = function() { if (req.readyState == 4) { if ((req.status >= 200 && req.status < 300) || req.status == 304) { ////alert(req.responseText); var jsonTextDiv = document.getElementById("jsonText"); // Deserializes JavaScript Object Notation (JSON) text to produce a JavaScript value. var data = JSON.parse(req.responseText); for (var i = 0; i < data.length; i++) { var item = data[i]; var div = document.createElement("div"); div.setAttribute("class", "dataItem"); // Inserts data into the html. div.innerHTML = item.Name + " sold " + item.Qty + "; Product number: " + item.SerialNumber; jsonTextDiv.appendChild(div); } } else { alert("Request was unsuccessful: " + req.status); } } }; // Sends a asyn request. req.open("GET", "ProductInfo.ashx", true); req.send(null); }}
由于前面我們介紹過(guò)Ajax發(fā)生請(qǐng)求的方法,所以不再重復(fù)介紹了,但我們注意到GetDataFromServer()方法中,獲取responseText數(shù)據(jù)(JSON格式),然后通過(guò)parse()方法把JSON格式數(shù)據(jù)轉(zhuǎn)換為Javascript對(duì)象,最后把數(shù)據(jù)插入到div中,頁(yè)面呈現(xiàn)效果如下:
圖3 Ajax請(qǐng)求結(jié)果
現(xiàn)在,我們成功地把數(shù)據(jù)輸出到頁(yè)面當(dāng)中,也許用戶還會(huì)覺(jué)得用戶體驗(yàn)不好,那么我們給就該頁(yè)面增加CSS樣式。
由于時(shí)間的關(guān)系,我們已經(jīng)把CSS樣式定義好了,具體如下:
#header { width: 100%; margin-left: 10px; margin-right: 10px; background-color:#480082; color: #FFFFFF;}body { margin-left: 40px; margin-right: 40px;}div#jsonText { background-color: #d9d9d9; -webkit-border-radius: 6px; border-radius: 6px; margin: 10px 0px 0px 0px; padding: 0px; border: 1px solid #d9d9d9;}div.dataItem { font-family: Verdana, Helvetica, sans-serif; color: #434343; padding: 10px;}div.dataItem:nth-child(2n) { background-color: #fafafa;}div.dataItem:first-child { -webkit-border-top-left-radius: 6px; -webkit-border-top-right-radius: 6px; border-top-left-radius: 6px; border-top-right-radius: 6px;}div.dataItem:last-child { -webkit-border-bottom-left-radius: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-left-radius: 6px; border-bottom-right-radius: 6px;}
我們刷新一下頁(yè)面,OK現(xiàn)在頁(yè)面效果好多了。
圖4 Ajax請(qǐng)求結(jié)果
上面我們獲取頁(yè)面和數(shù)據(jù)都是在同源請(qǐng)求情況下,也就是說(shuō),客戶端瀏覽器請(qǐng)求的頁(yè)面和數(shù)據(jù)都是屬于同一域名、同一端口和同協(xié)議。
同源策略:阻止從一個(gè)域上加載的腳本獲取或操作另一個(gè)域上的文檔屬性。也就是說(shuō),受到請(qǐng)求的URL的域必須與當(dāng)前Web頁(yè)面的域相同、相同端口。這意味著瀏覽器隔離來(lái)自不同源的內(nèi)容,以防止它們之間的操作。
圖5同源請(qǐng)求過(guò)程
在一些情況下,我們不可以避免地要地需要從其他域名或服務(wù)器中跨域請(qǐng)求數(shù)據(jù),但前面提到Ajax只能向同一個(gè)域中使用相同端口和協(xié)議的URL中發(fā)送請(qǐng)求;如果URL與啟動(dòng)請(qǐng)求的頁(yè)面有任何差別,都會(huì)引發(fā)安全錯(cuò)誤。
跨源策略(CORS):是一個(gè)Web瀏覽器技術(shù)規(guī)范,它定義了一個(gè)方法讓W(xué)eb服務(wù)器允許其他域名頁(yè)面訪問(wèn)它的資源??缭床呗远x了一個(gè)方法讓瀏覽器和服務(wù)器可以交互決定是否允許跨源請(qǐng)求。
圖6跨源請(qǐng)求過(guò)程
大家注意到同源請(qǐng)求中我們使用的是JSON格式,但在跨源請(qǐng)求中卻是使用JSONP,這時(shí)大家可能有點(diǎn)困惑,坦然我剛開始學(xué)習(xí)的時(shí)候也是這樣的。
首先我們必須理解JSON和JSONP的區(qū)別:JSON是一種數(shù)據(jù)格式,而JSONP像是通過(guò)一個(gè)方法名來(lái)封裝JSON格式;由于瀏覽器允許跨源請(qǐng)求
聯(lián)系客服