深入Atlas系列之客戶端支持
Atlas提供了強大而靈活的服務器端Web Services訪問能力。這對于客戶端AJAX開發提供了絕好的條件,這幾乎也是任何AJAX框架必備的功能。因為只要有了它,就能輕松地以AJAX方式與服務器端進行交互,而其他多樣的頁面操作自然可以由開發人員盡情開發。對于部分喜歡自己動手的開發人員來說,這甚至是他們僅僅需要的支持。
從這篇文章開始,我會從實現角度剖析Atlas對于Web Services的支持,希望能夠幫助大家更深入地理解,更靈活地使用Atlas提供的這一功能。
在Atlas中,對于Web Services的訪問,其實都是通過Sys.Net.ServiceMethod類來實現的。我們先通過UML來看一下Sys.Net.ServiceMethod以及其其他一些類的關系。

Sys.Net.WebMethod類是Sys.Net.ServiceMethod和Sys.Net.PageMethod的父類。后者用于訪問寫于頁面中使用WebMethodAttribute標注的方法,不在這篇文章的討論范圍內。Sys.Net.WebMethod內定義了五個抽象函數:get_methodName、addHeaders、get_url、get_body和get_appUrl,其作用應該相當地顯而易見。而繼承Sys.Net.WebMethod的類,例如Sys.Net.ServiceMethod,則提供了這五個抽象函數的實現。
Sys.Net.WebMethod類有一個非常有趣的函數“invoke”。從UML圖中會發現,它居然提供了一個Javascript中沒有的功能:“函數重載(overload)”!至于它是如何實現,該如何使用,稍后將結合代碼進行詳細介紹。
那么就進入代碼分析階段,先從Sys.Net.WebMethod的結構看起,從一個Atlas類的大致結構可以看出該類的成員定義和“構造函數”的實現。
Sys.Net.WebMethod結構:
可以見到五個抽象函數定義,在this._invoke函數是真正產生請求的地方,里面還有數個用于引發事件的方法。整個類的結構非常簡單。
接下來我們來仔細分析一下this.invoke函數。
this.invoke函數分析
可以看到,這就是就是this.invoke“函數重載(overload)”的實現方式。到底是故意如此設計還是在后續開發時為了兼容,可能就不得而知了。不過也就是說,我們現在有中參數傳遞可以使用。
第一種是:
this.invoke第一種調用方式:
第二種是:
this.invoke第二種調用方式:
關于參數的含義,請參照this._invoke函數的分析。
接下來分析this._invoke的代碼,這才是真正工作的代碼。
this._invoke函數分析:
可以看出,Sys.Net.WebMethod是使用Sys.Net.WebReqeust來發出AJAX請求的。在Atlas應用中如果需要使用AJAX請求的話,應該全部使用Sys.Net.WebRequest,這個類不僅對于XMLHttpRequest進行了良好的封裝,另外它使用了Sys.Net._WebRequestManager對于所有請求進行了全局地控制,使用了瀏覽器和HTTP協議的特性,提高了請求的效率。這一點幾乎是微軟介紹Atlas時都會著重強調的一點。
在創建了Sys.Net.WebRequest對象后,并不是將用戶傳入的那些回調函數直接注冊給它的事件,而是使用了Sys.Net.WebMethod里的onXXXX,進行了進一步的處理,代碼如下:
onXXX函數分析:
在onComplete方法中,會查看status code。在HTTP 1.x中,2xx代表了Success(關于完整Status Code的描述,請參考http://www.w3.org/Protocols/HTTP/HTRESP.html)。由此可以得知該如何調用用戶提供的回調函數。在調用回調函數時會將用戶提供的userContext作為參數傳入,這種做法在異步調用中被經常使用,例如.NET Framework中Delegate的異步調用。
至此,Sys.Net.WebMethod就被解釋完了,并不復雜,甚至我覺得我的解釋有些累贅。不過它提供的方法非常重要,是客戶端訪問服務器端函數的核心(客戶端方面)。而調用Web Services,則需要Sys.Net.ServiceMethod這個Sys.Net.WebMethod的子類來提供那五個抽象函數的具體實現,分析如下:
Sys.Net.ServiceMethod代碼分析:
對于傳入參數url和appUrl,可能需要重新解釋一下。如果url傳入的是相對路徑,則appUrl可以為null。
為了有更深的理解,我們來看一個例子:
ws.asmx代碼:
ws.asmx文件代碼:
HelloWorld函數接受一個整數作為參數,返回一個數組。第一個元素為一個字符串,第二個元素為服務器當前時間。
Default.aspx文件代碼:
Default.aspx文件代碼:
在這里使用的是invoke函數的第二種調用方法,傳入一個隨機數作為參數,并將信息顯示在頁面上。效果如下:
我們打開Fiddler,看看具體的請求如何,請注意紅色框出的地方:
Request:
Request Body:
Response Body:
是不是和我們預料的完全相同?有了JSON,我們可以非常方便地構造和表示一個客戶端對象,Atlas在客戶端和服務器端都提供了非常強大的JSON Serializer。這些方法可以應用在任何需要場合,即使脫離了Atlas。
到現在為止,已經將Atlas以AJAX方式調用Web Services的客戶端基礎代碼分析完了。但是這其實還遠遠不夠,有了客戶端代碼,至少還需要服務器端的支持。那么在服務器端Atlas又是如何提供以AJAX方式調用Web Services方法的功能呢?
我們將在下一篇文章中討論這個問題。
從這篇文章開始,我會從實現角度剖析Atlas對于Web Services的支持,希望能夠幫助大家更深入地理解,更靈活地使用Atlas提供的這一功能。
在Atlas中,對于Web Services的訪問,其實都是通過Sys.Net.ServiceMethod類來實現的。我們先通過UML來看一下Sys.Net.ServiceMethod以及其其他一些類的關系。

Sys.Net.WebMethod類是Sys.Net.ServiceMethod和Sys.Net.PageMethod的父類。后者用于訪問寫于頁面中使用WebMethodAttribute標注的方法,不在這篇文章的討論范圍內。Sys.Net.WebMethod內定義了五個抽象函數:get_methodName、addHeaders、get_url、get_body和get_appUrl,其作用應該相當地顯而易見。而繼承Sys.Net.WebMethod的類,例如Sys.Net.ServiceMethod,則提供了這五個抽象函數的實現。
Sys.Net.WebMethod類有一個非常有趣的函數“invoke”。從UML圖中會發現,它居然提供了一個Javascript中沒有的功能:“函數重載(overload)”!至于它是如何實現,該如何使用,稍后將結合代碼進行詳細介紹。
那么就進入代碼分析階段,先從Sys.Net.WebMethod的結構看起,從一個Atlas類的大致結構可以看出該類的成員定義和“構造函數”的實現。
Sys.Net.WebMethod結構:
| 1 Sys.Net.WebMethod = function() { 2 //抽象成員定義 3 this.addHeaders = Function.abstractMethod; 4 this.get_appUrl = Function.abstractMethod; 5 this.get_url = Function.abstractMethod; 6 this.get_body = Function.abstractMethod; 7 8 //最后這個定義是我補上的,很明顯代碼里缺少了這個定義 9 this.get_methodName = Function.abstractMethod; 10 11 this.invoke = function(params) { 12 …… 13 } 14 15 this._invoke = function(params, onMethodComplete, onMethodTimeout, 16 onMethodError, onMethodAborted, userContext, timeoutInterval, priority, useGetMethod) { 17 18 …… 19 20 function onComplete(response, eventArgs) { 21 …… 22 } 23 24 function onTimeout(request, eventArgs) { 25 …… 26 } 27 28 function onAborted(request, eventArgs) { 29 …… 30 } 31 32 …… 33 } 34 } 35 Sys.Net.WebMethod.registerAbstractClass('Sys.Net.WebMethod'); |
可以見到五個抽象函數定義,在this._invoke函數是真正產生請求的地方,里面還有數個用于引發事件的方法。整個類的結構非常簡單。
接下來我們來仔細分析一下this.invoke函數。
this.invoke函數分析
| 1 // 參數params是一個Dictionary,用key - value的方式 2 // 保存即將傳遞給Web Services的參數 3 this.invoke = function(params) { 4 var numOfParams = arguments.length; 5 6 // 如果有兩個參數,并且第二個參數不是一個函數, 7 // 則說明函數調用時參數是這樣的: 8 // this.invoke(params, settings); 9 if (numOfParams == 2 && arguments[1] && typeof(arguments[1]) != 'function') { 10 11 // 構造一個數組,用于獲得每個key的index 12 var expectedParamNames = 13 ["onMethodComplete", "onMethodTimeout", "onMethodError", 14 "onMethodAborted", "userContext", "timeoutInterval", 15 "priority", "useGetMethod"]; 16 17 // 傳入的第二個參數settings 18 var paramContainer = arguments[1]; 19 20 // 將要傳遞給this._invoke函數的參數數組 21 var newParams = new Array(expectedParamNames.length + 1); 22 // 第一個參數就是params 23 newParams[0] = params; 24 25 // 枚舉settings的每個key 26 for (var paramName in paramContainer) { 27 // 由于傳遞給this._invoke時參數需要按照順序順序, 28 // 所以必須獲得當前key的index。 29 var index = expectedParamNames.indexOf(paramName); 30 31 // 如果setting里有個key是不需要的,那么拋出Error 32 if (index < 0) { 33 throw Error.createError( 34 String.format( 35 "'{0}' is not a valid argument. It should be one of {1}", 36 paramName, expectedParamNames 37 ) 38 ); 39 } 40 41 // 將參數放在數組合適的位置上 42 newParams[index + 1] = paramContainer[paramName]; 43 } 44 45 // 將準備好的參數數組傳遞給this._invoke調用 46 return this._invoke.apply(this, newParams); 47 } 48 49 // 還有一種調用方式的參數和this_invoke的參數完全相同 50 return this._invoke.apply(this, arguments); 51 } |
可以看到,這就是就是this.invoke“函數重載(overload)”的實現方式。到底是故意如此設計還是在后續開發時為了兼容,可能就不得而知了。不過也就是說,我們現在有中參數傳遞可以使用。
第一種是:
this.invoke第一種調用方式:
| 1 this.invoke( 2 { 3 param1 : value1, 4 param2 : value2, 5 …… 6 }, 7 { 8 onMethodComplete : ……, 9 onMethodTimeout : ……, 10 onMethodError : ……, 11 onMethodAborted : ……, 12 userContext : ……, 13 timeoutInterval : ……, 14 priority : ……, 15 useGetMethod : …… 16 }); |
第二種是:
this.invoke第二種調用方式:
| 1 this.invoke( 2 { 3 param1 : value1, 4 param2 : value2, 5 …… 6 }, 7 onMethodComplete, 8 onMethodTimeout, 9 onMethodError, 10 onMethodAborted, 11 userContext, 12 timeoutInterval, 13 priority, 14 useGetMethod); |
關于參數的含義,請參照this._invoke函數的分析。
接下來分析this._invoke的代碼,這才是真正工作的代碼。
this._invoke函數分析:
| 1 // 參數定義: 2 // params:一個Dictionary,用key - value的方式保存即將傳遞給Web Services的參數 3 // onMethodComplete:調用完成時使用的回調函數 4 // onMethodTimeout:請求超時時使用的回調函數 5 // onMethodError:Web Services發生錯誤(例如拋出異常)時使用的回調函數 6 // onMethodAborted:請求被取消是使用得回調函數 7 // userContext:用戶提供的任意參數,會在回調函數被執行時作為參數使用 8 // timeoutInterval:超時前所等待的時間,Number類型 9 // priority:優先級,Sys.Net.WebRequestPriority枚舉類型 10 // useGetMethod:是否使用HTTP GET方法,Boolean類型 11 this._invoke = function(params, onMethodComplete, 12 onMethodTimeout, onMethodError, onMethodAborted, userContext, 13 timeoutInterval, priority, useGetMethod) { 14 15 // 檢測參數類型是否正確 16 window.debug.validateParameters("WebMethod.Invoke", arguments, 17 [ 18 ['params', Object, true], 19 ['onMethodComplete', Function, true], 20 ['onMethodTimeout', Function, true], 21 ['onMethodError', Function, true], 22 ['onMethodAborted', Function, true], 23 ['timeoutInterval', Number, true], 24 ['priority', Number, true], 25 ['useGetMethod', Boolean, true] 26 ]); 27 28 // 使用Sys.Net.WebRequest對象進行AJAX請求 29 var request = new Sys.Net.WebRequest(); 30 31 // 使用子類的addHeaders實現添加Header 32 this.addHeaders(request.get_headers()); 33 // 使用子類的實現set_url實現設置url 34 request.set_url(this.get_url(params, useGetMethod)); 35 // 使用子類的實現set_appUrl實現設置appUrl 36 request.set_appUrl(this.get_appUrl()); 37 38 // 為了添加body,param不能為null 39 if (params == null) { 40 params = {}; 41 } 42 43 // 使用子類的set_body實現設置body 44 request.set_body(this.get_body(params, useGetMethod)); 45 // 將onComplete函數注冊給Sys.Net.WebRequest的complete事件 46 request.completed.add(onComplete); 47 // 將onTimeout函數注冊給Sys.Net.WebRequest的timeout事件 48 request.timeout.add(onTimeout); 49 // 將onAborted函數注冊給Sys.Net.WebRequest的aborted事件 50 request.aborted.add(onAborted); 51 52 // 如果提供了timeoutInterval那么設置它 53 if (timeoutInterval) { 54 request.set_timeoutInterval(timeoutInterval); 55 } 56 57 // 如果priority不是Sys.Net.WebRequestPriority.High的話, 58 // 則設置WebRequest的Priority屬性 59 if (priority >= 0) { 60 request.set_priority(priority); 61 } 62 63 // 獲得methodName,為后面的onXXXX方法提供信息 64 var methodName = this.get_methodName(); 65 66 request.invoke(); 67 68 function onComplete(response, eventArgs) { 69 …… 70 } 71 72 function onTimeout(request, eventArgs) { 73 …… 74 } 75 76 function onAborted(request, eventArgs) { 77 …… 78 } 79 80 //返回Sys.Net.WebRequest對象,一般沒有什么作用 81 return request; 82 } |
可以看出,Sys.Net.WebMethod是使用Sys.Net.WebReqeust來發出AJAX請求的。在Atlas應用中如果需要使用AJAX請求的話,應該全部使用Sys.Net.WebRequest,這個類不僅對于XMLHttpRequest進行了良好的封裝,另外它使用了Sys.Net._WebRequestManager對于所有請求進行了全局地控制,使用了瀏覽器和HTTP協議的特性,提高了請求的效率。這一點幾乎是微軟介紹Atlas時都會著重強調的一點。
在創建了Sys.Net.WebRequest對象后,并不是將用戶傳入的那些回調函數直接注冊給它的事件,而是使用了Sys.Net.WebMethod里的onXXXX,進行了進一步的處理,代碼如下:
onXXX函數分析:
| 1 function onComplete(response, eventArgs) { 2 // 獲得Status Code 3 var statusCode = response.get_statusCode(); 4 5 var result = null; 6 7 8 try { 9 // 嘗試將Data序列化成對象 10 result = response.get_object(); 11 } 12 catch (ex) { // 失敗了,說明不是傳出對象 13 try { 14 // 獲得XML 15 result = response.get_xml(); 16 } 17 catch (ex) { } 18 } 19 20 // 如果沒有成功(statusCode不是2XX),或者 21 // result為Sys.Net.MethodRequestError類型, 22 // 表示Web Services出錯了(例如拋出了Exception) 23 if (((statusCode < 200) || (statusCode >= 300)) || 24 Sys.Net.MethodRequestError.isInstanceOfType(result)) { 25 26 // 如果提供了onMethodError回調函數,那么執行 27 if (onMethodError) { 28 onMethodError(result, response, userContext); 29 } 30 else { 31 // 沒有提供onMethodError的話那么就Trace 32 debug.trace("The server method '" + methodName + "' failed with the following error:"); 33 34 if (result != null) { 35 debug.trace(result.get_exceptionType() + ": " + result.get_message()); 36 } 37 else { 38 debug.trace(response.get_data()); 39 } 40 } 41 } 42 else if (onMethodComplete) { // 調用成功了 43 // 如果提供了onMethodComplete回調函數,那么執行 44 onMethodComplete(result, response, userContext); 45 } 46 } 47 48 function onTimeout(request, eventArgs) { 49 if (onMethodTimeout) { 50 onMethodTimeout(request, userContext); 51 } 52 } 53 54 function onAborted(request, eventArgs) { 55 if (onMethodAborted) { 56 onMethodAborted(request, userContext); 57 } 58 } |
在onComplete方法中,會查看status code。在HTTP 1.x中,2xx代表了Success(關于完整Status Code的描述,請參考http://www.w3.org/Protocols/HTTP/HTRESP.html)。由此可以得知該如何調用用戶提供的回調函數。在調用回調函數時會將用戶提供的userContext作為參數傳入,這種做法在異步調用中被經常使用,例如.NET Framework中Delegate的異步調用。
至此,Sys.Net.WebMethod就被解釋完了,并不復雜,甚至我覺得我的解釋有些累贅。不過它提供的方法非常重要,是客戶端訪問服務器端函數的核心(客戶端方面)。而調用Web Services,則需要Sys.Net.ServiceMethod這個Sys.Net.WebMethod的子類來提供那五個抽象函數的具體實現,分析如下:
Sys.Net.ServiceMethod代碼分析:
| 1 // 如果要訪問的Web Services是http://www.sample.com/abc/ws.amsx中 2 // HelloWorld方法的話,則 3 // url:Web Services的地址,以上例為"/abc/ws.amsx" 4 // methodName:方法名,以上例為"HelloWorld" 5 // appUrl:該Web應用程序的URL,以上例(可能,也有可能不是)為"http://www.sample.com/" 6 Sys.Net.ServiceMethod = function(url, methodName, appUrl) { 7 Sys.Net.ServiceMethod.initializeBase(this); 8 9 this.get_methodName = function() { return methodName; } 10 11 // header:一個以Dictionary,用key - value的方式 12 // 表示Header的對象,Object類型 13 this.addHeaders = function(headers) { 14 // Content-Type設為"application/json", 15 // 表示要通過JSON方式來傳遞數據 16 headers['Content-Type'] = 'application/json'; 17 } 18 19 // param:一個Dictionary,用key - value的方式 20 // 保存即將傳遞給Web Services的參數。 21 // useGetMethod:是否使用HTTP GET方法,Boolean類型 22 this.get_url = function(params, useGetMethod) { 23 // 如果不是用HTTP GET方法,或者沒有提供params, 24 if (!useGetMethod || !params) 25 { 26 // 將params設為空對象,params將被作為Query String添加在 27 // URL上,自然使用HTTP POST方法時不需要那些Query String 28 params = {}; 29 } 30 31 // 添加一個mn,值為methodName, 32 // 這句代碼等價于params["mn"] = methodName; 33 params.mn = methodName; 34 // 調用Sys.Net.WebRequest.createUrl方法獲得url, 35 // 將params作為Query String放在url后。 36 // 在這里,正常情況下只有mn一個參數。 37 var fullUrl = Sys.Net.WebRequest.createUrl(url, params); 38 // 刪除mn 39 delete params.mn; 40 return fullUrl; 41 } 42 43 this.get_body = function(params, useGetMethod) { 44 // 如果使用HTTP GET方法,則不用body 45 if (useGetMethod) return null; 46 47 // 將params序列化作為body 48 var body = Sys.Serialization.JSON.serialize(params); 49 50 // 如果是空對象,則返回空body。 51 if (body == "{}") return ""; 52 53 return body; 54 } 55 56 this.get_appUrl = function() { 57 return appUrl; 58 } 59 } 60 Sys.Net.ServiceMethod.registerClass('Sys.Net.ServiceMethod', Sys.Net.WebMethod); |
對于傳入參數url和appUrl,可能需要重新解釋一下。如果url傳入的是相對路徑,則appUrl可以為null。
為了有更深的理解,我們來看一個例子:
ws.asmx代碼:
ws.asmx文件代碼:
| 1 <%@ WebService Language="C#" Class="ws" %> 2 3 using System; 4 using System.Web; 5 using System.Web.Services; 6 using System.Web.Services.Protocols; 7 8 [WebService(Namespace = "http://tempuri.org/")] 9 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] 10 public class ws : System.Web.Services.WebService { 11 12 [WebMethod] 13 public object[] HelloWorld(int param1) { 14 return new object[] { "You passed parameter: " + param1, DateTime.Now }; 15 } 16 } |
HelloWorld函數接受一個整數作為參數,返回一個數組。第一個元素為一個字符串,第二個元素為服務器當前時間。
Default.aspx文件代碼:
Default.aspx文件代碼:
| 1 <%@ Page Language="C#" %> 2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 3 4 <html xmlns="http://www.w3.org/1999/xhtml" > 5 <head runat="server"> 6 <title>Web Service Call by Sys.Net.ServiceMethod</title> 7 <script language="javascript"> 8 function invoke() 9 { 10 var params = { "param1" : Math.round(Math.random() * 100) }; 11 var method = new Sys.Net.ServiceMethod("ws.asmx", "HelloWorld", null); 12 13 method.invoke(params, onMethodComplete); 14 } 15 16 function onMethodComplete(result, response, userContext) 17 { 18 document.getElementById("display").innerHTML = 19 result[0] + "<br />" + "Server Time: " + result[1]; 20 } 21 </script> 22 </head> 23 <body style="font-family:Arial;"> 24 <form id="form1" runat="server"> 25 <div> 26 <atlas:ScriptManager runat="server" ID="ScriptManager1" EnableScriptComponents="true" /> 27 28 <div id="display"></div> 29 30 <input onclick="invoke();" value="Invoke" type="button" /> 31 </div> 32 </form> 33 </body> 34 </html> |
在這里使用的是invoke函數的第二種調用方法,傳入一個隨機數作為參數,并將信息顯示在頁面上。效果如下:
![]() |
我們打開Fiddler,看看具體的請求如何,請注意紅色框出的地方:
Request:
![]() |
Request Body:
![]() |
Response Body:
![]() |
是不是和我們預料的完全相同?有了JSON,我們可以非常方便地構造和表示一個客戶端對象,Atlas在客戶端和服務器端都提供了非常強大的JSON Serializer。這些方法可以應用在任何需要場合,即使脫離了Atlas。
到現在為止,已經將Atlas以AJAX方式調用Web Services的客戶端基礎代碼分析完了。但是這其實還遠遠不夠,有了客戶端代碼,至少還需要服務器端的支持。那么在服務器端Atlas又是如何提供以AJAX方式調用Web Services方法的功能呢?
我們將在下一篇文章中討論這個問題。



