top
Loading...
深入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結構:

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方法的功能呢?

我們將在下一篇文章中討論這個問題。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗