編寫輕量ajax組件02-AjaxPro淺析

前言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組件的源代碼,有興趣的朋友能夠研究研究。

相關文章
相關標籤/搜索