前言html
上一篇介绍了在webform平台实现ajax的一些方式,而且实现一个基类。这一篇咱们来看一个开源的组件:ajaxpro。虽然这是一个比较老的组件,不过实现思想和源码仍是值得咱们学习的。经过上一篇的介绍,咱们知道要调用页面对象的方法,就是靠反射来实现的,关键是整个处理过程,包括反射调用方法、参数映射等。ajaxpro不只在后台帮咱们实现了这个过程,在前台也封装了请求调用的方法,例如ajax的相关方法,用ajaxpro的方法就能够发送异步请求了,不须要本身封装js或者使用js库。接下来就对这个组件进行浅析。前端
1、ajaxpro的使用jquery
咱们先来看这个组件如何使用。web
1. 注册AjaxHandlerFactoryajax
在web.config里进行以下配置:数组
1
2
3
|
<httpHandlers>
<add verb=
"POST,GET"
path=
"ajaxpro/*.ashx"
type=
"AjaxPro.AjaxHandlerFactory, AjaxPro"
/>
</httpHandlers>
|
简单的说,请求的url符合 ajaxpro/*.ashx 格式的,都会被AjaxHandlerFactory处理,这是一个实现IHandlerFactory接口的工厂类,用来获取IHandler处理程序。其中type的格式是:"名称控件.类名称,程序集名称"。浏览器
2. 在页面类Page_Load事件进行注册缓存
1
2
3
4
|
protected
void
Page_Load(
object
sender, EventArgs e)
{
AjaxPro.Utility.RegisterTypeForAjax(
typeof
(AjaxProPage));
}
|
咱们传递了本页面对象的Type给ResisterTypoForAjax方法,这个方法用来在前台注册脚本,具体会调用当前Page对象的RegisterClientScriptBlock进行注册,因此.aspx文件中必须有一个<form runat="server"></form>,不然脚本将没法注册。(这里传递了Type,实际也能够作到不用传递的,内部经过HttpContext.Current.Handler.GetType().BaseType 也能够得到这个类型)session
3.用AjaxMethod标记方法 mvc
1
2
3
4
5
|
[AjaxMethod]
public
List<
string
> GetList(
string
input1,
string
input2)
{
return
new
List<
string
> { input1, input2 };
}
|
AjaxMethod是一个标记属性,表示这个方法用于处理ajax请求,它最终经过反射执行;它有几个构造函数对,对于有些须要缓存的数据,能够设置缓存时间;若是咱们的请求不须要使用Session,能够设置HttpSessionStateRequirement;若是请求须要异步,例如请求一个耗时的web服务,也能够设置处理程序为异步状态。
方法的返回值能够是简单的类型,也能够是复杂的类型;例如集合类型在前台得到就是一个数组。
4.前台调用
后台的配置和使用都很是简单,接下来咱们看前台如何发起请求。
1
2
3
4
5
6
7
|
function GetList() {
//var result = AjaxProNamespace.AjaxProPage.GetList("a", "b").value;
//console.log(result);
AjaxProNamespace.AjaxProPage.GetList(
"a"
,
"b"
, function (result) {
console.log(result);
});
}
|
这里AjaxProNamespace 是页面类所在的名称空间,AjaxProPage 就是页面类的名称,GetList是标记的方法。为何能够这样写呢?前面说到,ajaxpro会在前台注册脚本,它会根据咱们页面对象的相关信息生成以下脚本,因此咱们才能够这样调用,而彻底不用本身写js或者用jquery库的方法。
1
2
3
4
5
6
7
8
9
10
|
if
(
typeof
AjaxProNamespace ==
"undefined"
) AjaxProNamespace={};
if
(
typeof
AjaxProNamespace.AjaxProPage_class ==
"undefined"
) AjaxProNamespace.AjaxProPage_class={};
AjaxProNamespace.AjaxProPage_class = function() {};
Object.extend(AjaxProNamespace.AjaxProPage_class.prototype, Object.extend(
new
AjaxPro.AjaxClass(), {
GetList: function(input1, input2) {
return
this
.invoke(
"GetList"
, {
"input1"
:input1,
"input2"
:input2},
this
.GetList.getArguments().slice(2));
},
url:
'/ajaxpro/AjaxProNamespace.AjaxProPage,TestAjaxProSourceCode.ashx'
}));
AjaxProNamespace.AjaxProPage =
new
AjaxProNamespace.AjaxProPage_class();
|
GetList的参数对应后台方法的参数,类型必须能够转换,不然调用会失败。最后一个参数为回调函数,回调函数的参数是对返回结果进行封装的对象,其value属性就是执行成功返回的值,如上面返回的就是一个数组对象。其error包括了失败的信息。
注意,上面注释掉的部分是同步请求的作法,这每每不是咱们想要的,我曾经就见过有人这样错误的使用。
2、ajaxpro处理请求原理
这里主要关注组件处理ajax请求的过程,其它辅助功能不作介绍。
1.生成辅助脚本
在Page_Load事件里咱们调用了AjaxPro.Utility.RegisterTypeForAjax(typeof(AjaxProPage)); 用来注册所须要的脚本。咱们注意到在前台页面引入了以下脚本:
也就是每一个页面都会都会发起这几个Get 请求。这几个都是.ashx结尾的文件,但实际里面都是js代码;这些js有的是做为资源嵌套在dll内部,有的是自动生成的,主要是封装了ajax请求相关方法,以及让咱们能够用:名称空间.页面类名称.标记方法名称 这样去调用方法。为何要用.ashx而不是用.js呢?由于做为组件内部的资源文件,外部没法直接请求.js文件,而.ashx能够被拦截,而后用Response.Write将内容输出。
若是每次都生成和发送这些脚本的效率是很低的,ajaxpro内部的处理是判断请求头的If-None-Math和If-Modified-Since,若是两个都和缓存的同样,就返回一个304状态码。因此,客户端只有首次请求服务端会返回文件的内容,后续的都只返回304表示使用本地缓存。咱们刷新页面能够验证这个过程:
咱们知道304状态码表示服务端告诉浏览器可使用本地缓存,它的具体过程是这样的:浏览器将发送请求,Request包括If-None-Math和If-Modified-Since;服务端接收到请求后,判断If-None-Math和ETag是否同样,判断If-Modified-Since和请求内容的Last-Modified-Time是否同样;若是都同样,则返回304状态码,浏览器接收到304,就直接使用本地缓存;若是有一个不同,服务端都将输出具体内容,此时Response包含新的ETag和Last-Modified-Time。这个过程最明显的好处就是服务端不须要发送内容给浏览器,但缺点就是浏览器和服务端还须要一次请求-响应的过程。我的认为这里可使用Cache-Control,并设置一个较大值的时间,由于这里的js文件内容基本是不会变化的。Cache-Control表示浏览器请求时,先判断请求是否过期,若是没有过期,则直接从本地缓存得到,这个过程浏览器不须要和服务端创建任何请求;若是过期,浏览器才会发起请求。(须要注意的是,浏览器缓存都是基于Get请求的,Post请求是不会被缓存的)
2. 拦截请求
HttpHandler(IHttpHandler) 和 HttpModule(IHttpModule) 是asp.net 两个重要的组件,让咱们能够在asp.net的基础上很方便的进行扩展。HttpHandler对应某种具体的请求,例如.ashx,.aspx等;HttpModule是一个拦截器,能够在管道的某个事件对全部请求进行拦截。简单的说,在管道中,HttpApplication会触发一系列事件,咱们在经过HttpModule对某个事件进行注册,例如咱们能够在处理程序对象生成前拦截请求,而后映射到本身的处理程序;而实际处理请求返回结果的是HttpHandler,例如Page用来生成html。
以asp.net mvc框架为例,它是创建在asp.net 路由机制的基础上的,asp.net 路由系统经过一个UrlRoutingModule对请求进行拦截,具体是在PostResolveRequestCache事件进行拦截,对url进行解析,封装相应的路由数据后,最终将请求交给一个MvcHandler进行处理,MvcHandler实现了IHttpHandler接口。
前面咱们进行了以下配置:<add verb="POST,GET" path="ajaxpro/*.ashx" type="AjaxPro.AjaxHandlerFactory, AjaxPro"/> 这代表了任何的以 ajaxpro/任意名称.ashx结尾的 Post/Get 请求,都交给AjaxPro.AjaxHandlerFactory进行处理,它是一个实现了IHandlerFactory的处理程序工厂,用来生成具体的IHttpHandler。组件内部定义了多个实现IHttpHandler的类,有的是为了生成js脚本的,对于处理ajax请求,主要分为两类:异步(IHttpAsyncHandler)和非异步(IHttpHandler);在这两类的基础上,对于Session的状态的支持又分为三种:支持读写(实现IRequiresSessionState标记接口)的Handler、只读(实现IReadOnlySessionState标记接口)的Handler和不支持Session的Handler。具体生成什么样的Handler是经过AjaxMethod进行判断的。
IHttpHandler的ProcessRequest(异步就是BeginProcessRequest)就用来执行请求返回输出结果的。若是只须要一种处理程序咱们也能够实现IHttpHandler。IHandlerFactory的定义以下:
1
2
3
4
5
|
public
interface
IHttpHandlerFactory
{
IHttpHandler GetHandler(HttpContext context,
string
requestType,
string
url,
string
pathTranslated);
void
ReleaseHandler(IHttpHandler handler);
}
|
因此,ajaxpro的全部请求都会符合ajaxpro/*.ashx格式,而后在GetHandler方法,就能够进行具体的处理,返回结果是IHttpHandler;以非异步状态为例,若是咱们配置了须要Session,就会生成一个实现IHttpHandler和IRequiresSessionState的Handler,若是须要只读的Session,就会生成一个实现IHttpHandler和IReadOnlySessionState的Handler;这些信息能够经过反射从AjaxMethod标记属性得到。AjaxHandlerFactory的主要代码以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
public
IHttpHandler GetHandler(HttpContext context,
string
requestType,
string
url,
string
pathTranslated)
{
string
filename = Path.GetFileNameWithoutExtension(context.Request.Path);
Type t =
null
;
Exception typeException =
null
;
bool
isInTypesList =
false
;
switch
(requestType)
{
//Get请求,获取前面的那4个脚本
case
"GET"
:
switch
(filename.ToLower())
{
case
"prototype"
:
return
new
EmbeddedJavaScriptHandler(
"prototype"
);
case
"core"
:
return
new
EmbeddedJavaScriptHandler(
"core"
);
case
"ms"
:
return
new
EmbeddedJavaScriptHandler(
"ms"
);
case
"prototype-core"
:
case
"core-prototype"
:
return
new
EmbeddedJavaScriptHandler(
"prototype,core"
);
case
"converter"
:
return
new
ConverterJavaScriptHandler();
default
:
return
new
TypeJavaScriptHandler(t);
}
case
"POST"
:
IAjaxProcessor[] p =
new
IAjaxProcessor[2];
p[0] =
new
XmlHttpRequestProcessor(context, t);
p[1] =
new
IFrameProcessor(context, t);
for
(
int
i = 0; i < p.Length; i++)
{
if
(p[i].CanHandleRequest)
{
//获取标记方法的AjaxMethod属性
AjaxMethodAttribute[] ma = (AjaxMethodAttribute[])p[i].AjaxMethod.GetCustomAttributes(
typeof
(AjaxMethodAttribute),
true
);
bool
useAsync =
false
;
HttpSessionStateRequirement sessionReq = HttpSessionStateRequirement.ReadWrite;
if
(ma.Length > 0)
{
useAsync = ma[0].UseAsyncProcessing;
if
(ma[0].RequireSessionState != HttpSessionStateRequirement.UseDefault)
sessionReq = ma[0].RequireSessionState;
}
//6种Handler,根据是否异步,session状态返回指定的Handler
switch
(sessionReq)
{
case
HttpSessionStateRequirement.Read:
if
(!useAsync)
return
new
AjaxSyncHttpHandlerSessionReadOnly(p[i]);
else
return
new
AjaxAsyncHttpHandlerSessionReadOnly(p[i]);
case
HttpSessionStateRequirement.ReadWrite:
if
(!useAsync)
return
new
AjaxSyncHttpHandlerSession(p[i]);
else
return
new
AjaxAsyncHttpHandlerSession(p[i]);
case
HttpSessionStateRequirement.None:
if
(!useAsync)
return
new
AjaxSyncHttpHandler(p[i]);
else
return
new
AjaxAsyncHttpHandler(p[i]);
default
:
if
(!useAsync)
return
new
AjaxSyncHttpHandlerSession(p[i]);
else
return
new
AjaxAsyncHttpHandlerSession(p[i]);
}
}
}
break
;
}
return
null
;
}
|
3. 反射执行方法
当得到一个处理本次请求的Handler后,就能够在其ProcessRequest(异步为BeginProcessRequest)执行指定的方法。要执行一个页面对象的方法,咱们必须知道指定页面所在的程序集,名称空间,页面类的名称以及方法的名称。这彷佛符合咱们前面:名称空间.类名称.方法名称的调用方式。为了与通常请求区分开,让组件具备足够的独立性,ajaxpro只拦截符合"ajaxpro/*.ashx格式的请求,这说明咱们的ajax请求也要符合这个格式。如:http://localhost:50712/ajaxpro/AjaxProNamespace.AjaxProPage,TestAjaxProSourceCode.ashx,这个格式由前台脚本自动生成,并不须要咱们去构造。仔细观察,会发现AjaxProNamespace.AjaxProPage,TestAjaxProSourceCode 就是页面类的彻底限定名:名称空间.类名称,程序集名称,经过这个咱们就能够生成具体的Type,而后进行反射获取信息。那么方法的名称呢?ajaxpro将其放在http header 中,名称为:X-AjaxPro-Method。有了这些信息,就能够反射执行方法了。这里核心代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
internal
void
Run()
{
try
{
//设置输出结果不缓存(这不必定是咱们想要的)
p.Context.Response.Expires = 0;
p.Context.Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);
p.Context.Response.ContentType = p.ContentType;
p.Context.Response.ContentEncoding = System.Text.Encoding.UTF8;
//验证ajax请求
if
(!p.IsValidAjaxToken())
{
p.SerializeObject(
new
System.Security.SecurityException(
"The AjaxPro-Token is not valid."
));
return
;
}
//方法参数对象数组
object
[] po =
null
;
//请求处理结果
object
res =
null
;
try
{
//获取参数
po = p.RetreiveParameters();
}
catch
(Exception ex){}
//获取缓存的Key
string
cacheKey = p.Type.FullName +
"|"
+ p.GetType().Name +
"|"
+ p.AjaxMethod.Name +
"|"
+ p.GetHashCode();
if
(p.Context.Cache[cacheKey] !=
null
)
{
//若是缓存存在,则直接使用缓存
p.Context.Response.AddHeader(
"X-"
+ Constant.AjaxID +
"-Cache"
,
"server"
);
p.Context.Response.Write(p.Context.Cache[cacheKey]);
return
;
}
try
{
if
(p.AjaxMethod.IsStatic)
{
//使用反射调用静态方法
try
{
res = p.Type.InvokeMember(
p.AjaxMethod.Name,
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.InvokeMethod,
null
,
null
, po);
}
catch
(Exception ex){}
}
else
{
try
{
//建立实例对象,反射调用实例方法
object
c = (
object
)Activator.CreateInstance(p.Type,
new
object
[] { });
if
(c !=
null
)
{
res = p.AjaxMethod.Invoke(c, po);
}
}
catch
(Exception ex){}
}
}
catch
(Exception ex){}
try
{
//判断结果是否是xml,如是设置ContentType
if
(res !=
null
&& res.GetType() ==
typeof
(System.Xml.XmlDocument))
{
p.Context.Response.ContentType =
"text/xml"
;
p.Context.Response.ContentEncoding = System.Text.Encoding.UTF8;
((System.Xml.XmlDocument)res).Save(p.Context.Response.OutputStream);
return
;
}
string
result =
null
; ;
System.Text.StringBuilder sb =
new
System.Text.StringBuilder();
try
{
result = p.SerializeObject(res);
}
catch
(Exception ex){}
//若是须要缓存,则将结果写入缓存
if
(p.ServerCacheAttributes.Length > 0)
{
if
(p.ServerCacheAttributes[0].IsCacheEnabled)
{
p.Context.Cache.Add(cacheKey, result,
null
, DateTime.Now.Add(p.ServerCacheAttributes[0].CacheDuration), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal,
null
);
}
}
}
catch
(Exception ex){}
}
catch
(Exception ex){}
}
|
3、总结
咱们总结一下ajaxpro的核心处理流程,它经过一个IHttpHandlerFactory拦截指定格式的url,而后从中获取类型的彻底限定名生成类型对象,接着经过反射获取标记方法的特性,生成一个自定义的实现IHttpHandler接口的对象;在其ProcessRequest方法中,从http headers获取方法名称,经过反射进行参数映射并执行函数。
ajaxpro 具备以下优势:
1. 配置简单。
2. 能够配合其它组件一块儿使用。
3. 封装前台脚本,咱们不用本身封装或者使用其它脚本库。
4. 对返回值处理,咱们能够返回简单类型或者复杂类型都会自动序列化。
缺点是:
1. 页面会多出4个请求。尽管会利用304缓存,但仍是须要一次请求-响应的过程。
2. ajax没法使用Get请求。因为自定义了url格式,使用这种格式就没法用Get请求了,咱们知道Get请求是能够被浏览器缓存的,雅虎前端优化建议中有一条就是多用get请求。事实上,应该把名称空间.类名称,程序集放到http header中,而后提供了一个type类型的参数让咱们自由选择。
3. 与<form runat="server">绑定。目的是用了为咱们生成前台脚本,但若是咱们但愿用.html文件 + .aspx.cs 的方式就不能用了(博客园有些页面就用了这种方式);甚至咱们的接口可能要给移动端使用,这种方便就变成了限制。
4. 反射。这样效率是比较低的,它甚至没有像咱们以前的页面类同样,对MethodInfo进行缓存。
能够看出,若是在不太计较效率的状况,这个组件仍是值得使用的。这里只是作一个核心的介绍,里面还有不少其它功能,这是ajaxpro组件的源代码,有兴趣的朋友能够研究研究。