深入Atlas系列:Web Sevices Access in Atlas(1) - 客户端支持
2006-10-10 02:43 by 老赵, 3933 visitsAtlas提供了强大而灵活的服务器端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类的大致结构可以看出该类的成员定义和“构造函数”的实现。
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函数。
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)”的实现方式。到底是故意如此设计还是在后续开发时为了兼容,可能就不得而知了。不过也就是说,我们现在有中参数传递可以使用。
第一种是:
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 });
第二种是:
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的代码,这才是真正工作的代码。
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,进行了进一步的处理,代码如下:
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的子类来提供那五个抽象函数的具体实现,分析如下:
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代码:
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文件代码:
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方法的功能呢?
我们将在下一篇文章中讨论这个问题。
点击这里查看范例。
点击这里下载范例。
写这样一片东西,居然花了我整整两倍的预计时间……