本文中我有三個(gè)目的。首先,我想提供一個(gè)AJAX風(fēng)格應(yīng)用程序的高級概述。其次,我想詳細(xì)地描述ASP.NET 2.0的異步回調(diào)機(jī)制。最后,我想對構(gòu)建AJAX風(fēng)格應(yīng)用程序的工具和框架的未來改進(jìn)作一下展望。
歸納來看,AJAX風(fēng)格的Web應(yīng)用程序展示了下列特征:
· 到Web服務(wù)器的異步請求-在用戶等待來自于Web服務(wù)器的響應(yīng)時(shí),瀏覽器用戶接口不會(huì)被堵塞,而是可以繼續(xù)響應(yīng)用戶的交互。
· 高度依賴于用JavaScript編寫的基于瀏覽器的邏輯-W3C DOM的最新改進(jìn)和標(biāo)準(zhǔn)化為實(shí)現(xiàn)動(dòng)態(tài)的客戶端UI更新提供了支持。
· 在瀏覽器和Web服務(wù)器之間的基于XML數(shù)據(jù)的交換-XMLHttp對象使得與Web服務(wù)器進(jìn)行通訊而不需要重載頁面成為可能。
一個(gè)AJAX應(yīng)用程序和傳統(tǒng)型Web應(yīng)用程序之間的最大差別是,每次用戶交互不會(huì)導(dǎo)致每一個(gè)HTTP請求都被發(fā)送到Web服務(wù)器;而是,用JavaScript實(shí)現(xiàn)的基于瀏覽器的邏輯掌握著控制權(quán),之后再由該控制決定是局部處理請求還是向服務(wù)器作異步調(diào)用。一旦到服務(wù)器的異步調(diào)用結(jié)束,客戶端邏輯立即適當(dāng)更新UI的相關(guān)部分。這種方式具有下列優(yōu)點(diǎn):
· 用戶體驗(yàn)更為豐富。例如,當(dāng)一個(gè)Google地圖用戶沿一個(gè)方向拖動(dòng)地圖時(shí),系統(tǒng)就會(huì)在后臺向服務(wù)器發(fā)出一個(gè)異步請求,結(jié)果是他能夠在超出屏幕邊界后繼續(xù)拖動(dòng)。這樣以來,當(dāng)用戶進(jìn)一步拖動(dòng)地圖時(shí),新的圖像已經(jīng)可用了。這導(dǎo)致一種響應(yīng)更快的感覺。
· 既然跨越基于XMLHttp的到服務(wù)器的調(diào)用狀態(tài)并沒有丟失,那么,AJAX應(yīng)用程序就可以避免每次都重新生成UI界面。
· 更多的邏輯位于瀏覽器端,從而減少了到Web服務(wù)器的來回請求的數(shù)量,進(jìn)而全面改進(jìn)系統(tǒng)的潛力。
盡管存在這么多的優(yōu)點(diǎn),然而AJAX風(fēng)格的應(yīng)用程序還是存在一些不足之處。例如,AJAX風(fēng)格應(yīng)用程序的開發(fā)是比較困難的,因?yàn)槿狈ο鄳?yīng)的框架(一組類似于Windows MFC工具包的UI類)和IDE(調(diào)試,可視化設(shè)計(jì),等等)支持。另外,基于AJAX進(jìn)行開發(fā)要求一個(gè)人必須至少掌握兩種語言(DHTML和JavaScript)。而且,AJAX風(fēng)格應(yīng)用程序的編碼需要更長的時(shí)間,因?yàn)樗枰硗獾臏y試以使其支持多瀏覽器版本和類型。最后,由于基于JavaScript的源碼為終端用戶可存取,所以開發(fā)過程中的安全分析也變得非常重要。
幸好,例如Atlas,AJAX.NET和Google Maps API等工具的出現(xiàn)為將來構(gòu)建AJAX風(fēng)格的應(yīng)用程序提供了更好的支持。接下來,我們將討論一下,對于構(gòu)建AJAX風(fēng)格應(yīng)用程序的支持技術(shù)的發(fā)展歷程以及我們能夠從最新發(fā)布的工具集Atlas得到怎樣的期望。
讓我們首先討論XMLHttp對象。這個(gè)對象最初為微軟所引入,以后在其它平臺(包括Mozilla和蘋果公司的Safari瀏覽器)上也得到實(shí)現(xiàn)。XMLHttp支持到Web服務(wù)器的異步請求,這樣可以允許客戶端基于JavaScript邏輯調(diào)用Web服務(wù)器而不需要重載整個(gè)頁面。
換句話說,在后臺與Web服務(wù)器的交互而不引起整個(gè)頁面重載是完全有可能的。
至于XMLHttp對象的使用則相當(dāng)直接。為簡單起見,讓我們僅考慮IE特定的語法。其實(shí),XMLHttp在其它瀏覽器上的實(shí)現(xiàn)語法與這里的討論也很類似。
request = new ActiveXObject("Microsoft.XMLHTTP"); if (request){ request.onreadystatechange = CallbackHandler; request.open("GET", URL, true); request.send(); } function CallbackHandler(){ if ((request.readyState == 4) && (request.status == 200){ string response = request.responseXML; //更新UI的相關(guān)部分 } } |
在上面的代碼片斷中,第一步實(shí)現(xiàn)實(shí)例化Microsoft.XMLHttp類。第二步設(shè)置我們剛剛創(chuàng)建的XMLHttp實(shí)例的屬性,其中包括當(dāng)XMLHttp請求完成時(shí)將得到控制的回調(diào)函數(shù)的地址。因?yàn)槲覀冊谙蚍⻊?wù)器作異步調(diào)用(通過把open方法的第三個(gè)參數(shù)設(shè)置為true來實(shí)現(xiàn)),所以我們需要回調(diào)函數(shù)的地址。在回調(diào)函數(shù)實(shí)現(xiàn)過程中,我們作額外的檢查以確保完成請求。
你從上面的示例代碼中可以看出,以獨(dú)立方式使用XMLHttp對象是相當(dāng)簡單的。然而,把XMLHttp集成到HttpPage生命周期的其它部分中是比較困難的-例如,如何確保服務(wù)器端的方法調(diào)用能夠存取頁面中其它控件的狀態(tài)呢?為了正確初始化這些控件的狀態(tài),服務(wù)器端的回調(diào)處理需要經(jīng)歷一個(gè)與回調(diào)過程類似的HttpPage生命周期。直接使用XMLHttp對象的其它挑戰(zhàn)是,作為開發(fā)者,我們需要考慮不同的瀏覽器類型。幸好,ASP.NET 2.0提供了一個(gè)可重用的模式-它能夠使得存取回調(diào)功能非常容易。注意,隨同ASP.NET 2.0一同發(fā)行了若干控件,包括GridView,TreeView等,都綜合利用了回調(diào)機(jī)制。
讓我們先看一下服務(wù)器端實(shí)現(xiàn)原理。首先,在服務(wù)器端要定義一個(gè)新的接口IcallBackEventHandler。任何ASPX頁面(或打算支持客戶端回調(diào)的控件)都需要實(shí)現(xiàn)這個(gè)ICallBackEventHandler接口。ICallBackEventHandler接口定義了一個(gè)稱為RaiseCallbackEvent的方法。這個(gè)方法使用一個(gè)字符串類型的參數(shù)并且返回一個(gè)字符串。
在客戶端,為了初始化回調(diào)功能,需要調(diào)用一個(gè)特殊的JavaScript函數(shù)。你可以通過調(diào)用ClientScriptManager.GetCallbackEventReference來獲得一個(gè)到這個(gè)特殊的JavaScript函數(shù)的引用。到GetCallbackEventReference的調(diào)用將產(chǎn)生一個(gè)回調(diào)引用。當(dāng)調(diào)用此回調(diào)函數(shù)時(shí),你只需要傳遞一個(gè)字符串類型的參數(shù)。這是與服務(wù)器端的RaiseCallbackEvent簽名一致的。這就是你在客戶端建立回調(diào)機(jī)制所需做的一切。其它的把客戶端回調(diào)函數(shù)鉤(hook up)到服務(wù)器端的IcallBackEventHandler接口的RaiseCallbackEvent方法的實(shí)現(xiàn)則是由框架來完成的。前面提到的初始化回調(diào)機(jī)制的特殊JavaScript函數(shù)使用了另外兩個(gè)參數(shù)(__CALLBACKPARAM和__CALLBACKID)作為回饋數(shù)據(jù),它們分別代表傳遞到調(diào)用者的字符串參數(shù)和控件的ID。在服務(wù)器端,ASP.NET檢測其它兩個(gè)參數(shù)的存在并且會(huì)把請求路由到適當(dāng)?shù)目丶,這將導(dǎo)致調(diào)用目標(biāo)控件上的RaiseCallbackEvent方法。為了解決前面提到的頁面上的控件的初始化問題,ASP.NET運(yùn)行時(shí)刻在服務(wù)一次回調(diào)時(shí)提供了一個(gè)簡化版本的HttpPage生命周期。這一周期包括瀏覽頁面初始化的某個(gè)具體階段,觀察狀態(tài)加載,頁面加載和回調(diào)函數(shù)事件處理等。一旦回調(diào)函數(shù)事件被控件所處理,HttpPage生命周期的其它階段就會(huì)被跳過。
為了幫助更好地理解ASP.NET 2.0的回調(diào)機(jī)制,發(fā)行包中包括了一個(gè)簡單的進(jìn)度條控件,它依靠回調(diào)來決定服務(wù)器確定的一項(xiàng)任務(wù)的狀態(tài)。下面的列表1顯示了該P(yáng)rogressBar控件的代碼。為了支持客戶端回調(diào)函數(shù),這個(gè)控件實(shí)現(xiàn)了ICallbackEventHandler接口。為了演示之目的,RaiseCallbackEvent方法實(shí)現(xiàn)簡單地查找存儲在會(huì)話中的一個(gè)計(jì)數(shù)器,每次給計(jì)數(shù)器加1,并且把新值返回到客戶端。最后,列表2顯示了負(fù)責(zé)初始化該回調(diào)函數(shù)的JavaScript代碼。它使用了this.Page.ClientScript.GetCallbackEventReference來獲得一個(gè)到需要初始化回調(diào)的函數(shù)的安全引用。
列表1:ProgressBar.cs
public class ProgressBar : System.Web.UI.Control, System.Web.UI.ICallbackEventHandler{ private int PercentCompleted{ get { if System.Web.HttpContext.Current.Session["PercentComplete"] == null) { System.Web.HttpContext.Current.Session["PercentComplete"] = 1; } else { System.Web.HttpContext.Current.Session["PercentComplete"] =(int)System.Web.HttpContext.Current.Session["PercentComplete"] + 1; } return (int)System.Web.HttpContext.Current.Session["PercentComplete"]; } set { System.Web.HttpContext.Current.Session["PercentComplete"] = 1; } } public string RaiseCallbackEvent(string eventArguments) { int percent = this.PercentCompleted; if (percent > 100) { this.PercentCompleted = 1; return "completed"; } else { return percent.ToString() + "%"; } } protected override void OnPreRender(EventArgs e) { this.Page.ClientScript.RegisterClientScriptBlock(typeof(ProgressBar), "ProgressBar", this.GetClientSideScript(), true); base.OnPreRender(e); } protected override void Render(HtmlTextWriter writer) { System.Text.StringBuilder sb = new StringBuilder(); sb.Append(@"<table id=""ProgressBarContainer"" bgcolor=""LightSteelBlue"" border=""0"" width=""400"" style=""DISPLAY:none; POSITION: absolute; Z-INDEX: 10"">"); sb.Append(@"<tr><td colspan=""3"" style=""padding:3px 2px 2px 10px"">"); sb.Append(@"<font face=""Verdana, Arial, Helvetica, sans-serif"" size=""2"">"); sb.Append(@"<span id=""ProgressBarLabel"">Uploading...</span>"); sb.Append(@"</font></td></tr><tr><td>"); sb.Append(@"<font size=""1""> </font></td><td bgcolor=""#999999"" width=""100%"">"); sb.Append(@"<table id=""ProgressBar"" border=""0"" width=""0"" cellspacing=""0"">"); sb.Append(@"<tr><td style=""background-image:url(progressbar.gif)""> <font size=""1""> </font></td>"); sb.Append(@"</tr></table></td>"); sb.Append(@"<td><font size=""1""> </font></td></tr>"); sb.Append(@"<tr height=""5px""><td colspan=""3""></td></tr>"); sb.Append(@"</table>"); writer.Write(sb.ToString()); base.Render(writer); } private string GetClientSideScript() { System.Reflection.Assembly dll = System.Reflection.Assembly.GetExecutingAssembly(); StreamReader reader; reader = new StreamReader(dll.GetManifestResourceStream("ProgressBar.txt")); StringBuilder js = new StringBuilder(reader.ReadToEnd()); string fp = this.Page.ClientScript.GetCallbackEventReference(this, "", "UpdateProgressBar", ""); js.Replace("##InitiateCallBack##", fp); reader.Close(); return js.ToString(); } } |
列表2:ProgressBar.js
<script language="javascript"> var isCompleted=false; //這個(gè)函數(shù)初始化到服務(wù)器端的回調(diào) function DrawProgressBar(){ ##InitiateCallBack##; if (!isCompleted) { window.setTimeout('DrawProgressBar()',200); } else { isCompleted=false; document.getElementById("ProgressBarContainer").style.display = 'none'; } } //當(dāng)thecallback完成時(shí),下列函數(shù)被調(diào)用 function UpdateProgressBar(percent){ if (percent == 'completed'){ isCompleted=true; } else{ document.getElementById("ProgressBar").width = percent; } } |
通過使用在ASP.NET 2.0提供的客戶端回調(diào)函數(shù),實(shí)現(xiàn)進(jìn)度條控件是比較直接的,因?yàn)樵诳丶涂蛻舳酥g傳遞的數(shù)據(jù)僅是一個(gè)簡單的字符串。然而,一旦我們把其它數(shù)據(jù)類型也添加到其中,我們就遇到在JavaScript和.NET類型系統(tǒng)之間不匹配的問題。遺憾的是,ASP.NET 2.0中的回調(diào)函數(shù)實(shí)現(xiàn)對此并無多大幫助。任何想使用多種數(shù)據(jù)類型(簡單類型和復(fù)雜類型)的應(yīng)用程序,都要實(shí)現(xiàn)一種自己的定制模式。
幸好,這種限制能夠通過使用一個(gè)AJAX.NET開源庫來加以克服,AJAX.NET實(shí)現(xiàn)了一種基于代理的方式來調(diào)用服務(wù)器端函數(shù)。AJAX.NET定義了一種稱為AJAXMethod的定制屬性。當(dāng)一個(gè)服務(wù)器端方法用AJAXMethod加以修飾時(shí),一個(gè)基于JavaScript的客戶端代理將被HttpHandler(它是AJAX.NET庫的一部分)自動(dòng)生成。不同于ASP.NET 2.0,它支持單個(gè)參數(shù)的字符串類型以便用于回調(diào)實(shí)現(xiàn)。AJAX.NET支持整數(shù),字符串,雙精度數(shù),DateTime,DataSet等多種類型。
Bertrand Le Roy建議使用AJAX.NET來處理JavaScript和.NET類型系統(tǒng)之間的差別。他創(chuàng)建了一種稱為EcmaScriptObject的服務(wù)器端控件-它基于.NET技術(shù)重新創(chuàng)建了JavaScript類型系統(tǒng)。其想法是,用.NET重新生成一種客戶端對象圖。當(dāng)轉(zhuǎn)換發(fā)生在服務(wù)器端時(shí),這種方法顯得更有意義。
即使我們有了一種類型安全的方法來調(diào)用回調(diào)函數(shù),但是,我們還面臨其它的挑戰(zhàn)。JavaScript擔(dān)當(dāng)起了把AJAX應(yīng)用程序的各個(gè)部分組合到一起的"膠水"的作用。當(dāng)然,相應(yīng)地,對JavaScript的依賴性也進(jìn)一步增加。遺憾的是,盡管JavaScript是一種強(qiáng)有力且通用的語言,但是它并沒有實(shí)現(xiàn)面向?qū)ο蟮脑瓌t。這意味著,要實(shí)現(xiàn)代碼重用可能更為困難。當(dāng)然,可以使用一些技巧來使JavaScript看上去更象傳統(tǒng)的面向?qū)ο笳Z言。不過即使如此,要實(shí)現(xiàn)托管語言中的例如事件和代理等特征仍然相當(dāng)困難。
其它困難還包括:缺乏一個(gè)可重用框架來進(jìn)一步提高JavaScript的開發(fā)效率。如果有一種基于JavaScript的能夠隱蔽不同執(zhí)行環(huán)境區(qū)別的UI框架或許更好些。另外,如果能夠創(chuàng)建一組類,它們可以用一種安全的方式(相對于手工編碼SOAP包并使用XMLHttp來傳遞它們)來調(diào)用Web服務(wù),也會(huì)相當(dāng)不錯(cuò)。
最近來自微軟的Atlas工程許諾要重點(diǎn)解決這類問題。這是一種極大程度地簡化AJAX風(fēng)格開發(fā)的偉大嘗試。Atlas提供了一種新的JavaScript框架(注意,下面是基于微軟的一次初步宣布,以后有可能發(fā)生改變)-UI開發(fā)工具包。這其中包括:支持諸如拖放和數(shù)據(jù)綁定等特征的常用控件;調(diào)用Web服務(wù)的SOAP棧;隱蔽瀏覽器差別的瀏覽器兼容層;包括例如本地緩沖等內(nèi)容的客戶端構(gòu)建模塊。另外,ASP.NET團(tuán)隊(duì)還計(jì)劃為ASP.NET開發(fā)其它構(gòu)建模塊,例如配置管理,成員管理等,以便把它們用作Web服務(wù)端點(diǎn),從而實(shí)現(xiàn)可以直接從JavaScript中對Web服務(wù)進(jìn)行存取-例如可以容易地從客戶端存取個(gè)人信息。最后,Atlas工程還計(jì)劃擴(kuò)展JavaScript語法以便包括接口、生命周期管理和multicast事件。
據(jù)說,接下來的幾個(gè)月將是令A(yù)JAX開發(fā)者激動(dòng)的日子。因此,我非常希望本文能夠激起您對AJAX的興趣,并在你以后構(gòu)建下一代Web應(yīng)用程序時(shí)優(yōu)先考慮使用這一技術(shù)。