編寫輕量ajax組件01-對比webform平臺上的各類實現方式

前言javascript

  Asp.net WebForm 和 Asp.net MVC(簡稱MVC) 都是基於Asp.net的web開發框架,二者有很大的區別,其中一個就是MVC更加註重http本質,而WebForm試圖屏蔽http,爲此提供了大量的服務器控件和ViewState機制,讓開發人員能夠像開發Windows Form應用程序同樣,基於事件模型去編程。二者各有優缺點和適用情景,但MVC如今是許多Asp.net開發者的首選。css

  WebForm是創建在Asp.net的基礎上的,Asp.net提供了足夠的擴展性,咱們也能夠利用這些在WebForm下編寫像MVC同樣的框架,這個有機會再寫。說到WebForm不少人就會聯想到服務器控件(拖控件!!!),其實否則,咱們也能夠徹底不使用服務器控件,像MVC那樣關注html。WebForm要拋棄服務器控件,集中關注html,首先就要將<form runat="server"></form>標籤去掉,這個runat server 的form 是其PostBack機制的基礎。既然咱們要回歸到html+css+js,那麼意味着許多東西都要本身實現,例如處理Ajax請求。不像MVC那樣,WebForm開始的設計就將服務器控件做爲主要組成部分,若是不使用它,那麼只能利用它的擴展性去實現。html

  本系列就是實現一個基於WebForm平臺的輕量級ajax組件,主要分爲三個部分:前端

  1. 介紹WebForm下各類實現方式。java

  2. 分析ajaxpro組件。web

  3. 編寫本身的ajax組件。ajax

1、Ajax簡介數據庫

  異步容許咱們在不刷新整個頁面的狀況下,像服務器請求或提交數據。對於複雜的頁面,爲了請求一點數據而重載整個頁面顯然是很低效的,ajax就是爲了解決這個問題的。ajax的核心是XmlHttpRequest對象,經過該對象,以文本的形式向服務器提交請求。XmlHttpRequest2.0後,還支持提交二進制數據。編程

  ajax安全:出於安全考慮,ajax受同源策略限制;也就是隻能訪問同一個域、同一個端口的請求,跨域請求會被拒絕。固然有時候需求須要跨域發送請求,經常使用的跨域處理方法有CORS(跨域資源共享)和JSONP(參數式JSON)。json

  ajax數據交互格式:雖然Ajax核心對象XmlHttpRequest有"XML"字眼,但客戶端與服務器數據交換格式不侷限於xml,例如如今更可能是使用json格式。  

  ajax 也是有缺點的。例如對搜索引擎的支持不太好;有時候也會違背url資源定位的初衷。

2、Asp.net MVC 平臺下使用ajax

  在MVC裏,ajax調用後臺方法很是方便,只須要指定Action的名稱便可。

  前臺代碼:

<body>
    <h1>index</h1>
    <input type="button" value="GetData" onclick="getData()" />
    <span id="result"></span>
</body>
<script type="text/javascript">
    function getData() {
        $.get("GetData", function (data) {
            $("#result").text(data);
        });
    }
</script>

  後臺代碼:

     public class AjaxController : Controller
    {
        public ActionResult GetData()
        {
            if(Request.IsAjaxRequest())
            {
                return Content("data");
            }
            return View();
        }
    }

3、WebForm 平臺下使用ajax

  3.1 基於服務器控件包或者第三方組件

  這是基於服務器控件的,例如ajax toolkit工具包,或者像FineUI這樣的組件。web前端始終是由html+css+js組成的,只不過如何去生成的問題。原生的咱們能夠本身編寫,或者用一些前端插件;基於服務器控件的,都是在後臺生成的,一般效率也低一點。服務器組件會在前臺生成一系列代理,本質仍是同樣的,只不過控件封裝了這個過程,不須要咱們本身編寫。基於控件或者第三方組件的模式,在一些管理系統仍是挺有用的,訪問量不是很大,能夠快速開發。

  3.2 基於ICallbackEventHandler接口

  .net 提供了ICallbackEventHandler接口,用於處理回調請求。該接口須要用ClientScriptManager在前臺生成代理腳本,用於發送和接收請求,因此須要<form runat="server">標籤。

  前臺代碼:

<body>
    <form id="form1" runat="server">
    <div>        
        <input type="button" value="獲取回調結果" onclick="callServer()" />
        <span id="result" style="color:Red;"></span>
    </div>
    </form>
</body>
<script type="text/javascript">
    function getCallbackResult(result){
        document.getElementById("result").innerHTML = result;
    }
</script>

  後臺代碼:

    public partial class Test1 : System.Web.UI.Page, ICallbackEventHandler
    {        
        protected void Page_Load(object sender, EventArgs e)
        {
            //客戶端腳本Manager
            ClientScriptManager scriptMgr = this.ClientScript;

            //獲取回調函數,getCallbackResult就是回調函數
            string functionName = scriptMgr.GetCallbackEventReference(this, "", "getCallbackResult", "");

            //發起請求的腳本,callServer就是點擊按鈕事件的執行函數
            string scriptExecutor = "function callServer(){" + functionName + ";}";

            //註冊腳本
            scriptMgr.RegisterClientScriptBlock(this.GetType(), "callServer", scriptExecutor, true);
        }

        //接口方法
        public string GetCallbackResult()
        {
            return "callback result";
        }

        //接口方法
        public void RaiseCallbackEvent(string eventArgument)
        {
        }
    }

  這種方式有如下缺點:

  1. 實現起來較複雜,每一個頁面Load事件都要去註冊相應的腳本。

  2. 前臺會生成一個用於代理的腳本文件。

  3. 對於頁面交互複雜的,實現起來很是麻煩。

  4. 雖然是回調,可是此時頁面對象仍是生成了。

  3.3 使用通常處理程序

  通常處理程序實際上是一個實現了IHttpHandler接口類,與頁面類同樣,它也能夠用於處理請求。通常處理程序一般不用於生成html,也沒有複雜的事件機制,只有一個ProcessRequest入口用於處理請求。咱們能夠將ajax請求地址寫成.ashx文件的路徑,這樣就能夠處理了,並且效率比較高。

  要輸出文本內容只須要Response.Write(data)便可,例如,從數據庫獲取數據後,序列化爲json格式字符串,而後輸出。前面說到,通常處理程序不像頁面同樣原來生成html,若是要生成html,能夠經過加載用戶控件生成。如:

        public void ProcessRequest(HttpContext context)
        {
            Page page = new Page();
            Control control = page.LoadControl("~/PageOrAshx/UserInfo.ascx");
            if (control != null)
            {
                StringWriter sw = new StringWriter();
                HtmlTextWriter writer = new HtmlTextWriter(sw);
                control.RenderControl(writer);
                string html = sw.ToString();
                context.Response.Write(html);                
            }
        }

  這種方式的優勢是輕量、高效;缺點是對於交互多的須要定義許多ashx文件,加大了管理和維護成本。

  3.4 頁面基類

  將處理ajax請求的方法定義在頁面對象內,這樣每一個頁面就能夠專一處理本頁面相關的請求了。這裏有點須要注意。

  1.如何知道這個請求是ajax請求?

    經過請求X-Requested-With:XMLHttlRequest 能夠判斷,大部份瀏覽器的異步請求都會包含這個請求頭;也能夠經過自定義請求頭實現,例如:AjaxFlag:XHR

  2.在哪裏統一處理?

    若是在每一個頁面類裏判斷和調用是很麻煩的,因此將這個處理過程轉到一個頁面基類裏處理。

  3.如何知道調用的是哪一個方法?

    經過傳參或者定義在請求頭均可以,例如:MethodName:GetData。

  4.知道方法名稱了,如何動態調用?

    反射

  5.如何知道該方法能夠被外部調用?

    能夠認爲public類型的就能夠被外部調用,也能夠經過標記屬性標記。

  經過上面的分析,簡單實現以下  

  頁面基類:

    public class PageBase : Page
    {
        public override void ProcessRequest(HttpContext context)
        {
            HttpRequest request = context.Request;
            if (string.Compare(request.Headers["AjaxFlag"],"AjaxFlag",0) == 0)
            {
                string methodName = request.Headers["MethodName"];
                if (string.IsNullOrEmpty(methodName))
                {
                    EndRequest("MethodName標記不能爲空!");
                }
                Type type = this.GetType().BaseType;
                MethodInfo info = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
                if (info == null)
                {
                    EndRequest("找不到合適的方法調用!");
                }                
                string data = info.Invoke(this, null) as string;
                EndRequest(data);
            }
            base.ProcessRequest(context);
        }

        private void EndRequest(string msg)
        {
            HttpResponse response = this.Context.Response;
            response.Write(msg);
            response.End();
        }
    }

  頁面類:

    public partial class Test1 : PageBase
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public string GetData()
        {
            return "213";
        }
    }  

  前臺代碼:

    function getData(){
        $.ajax({
            headers:{"AjaxFlag":"XHR","MethodName":"GetData"},
            success:function(data){
                $("#result").text(data);
            }
        });
    }

4、優化版頁面基類

  上面的頁面基類功能不多,並且經過反射這樣調用的效率很低。這裏優化一下:

  1.能夠支持簡單類型的參數。

    例如上面的GetData能夠是:GetData(string name),經過函數元數據能夠獲取相關的參數,再根據請求的參數,就能夠設置參數了。

  2.加入標記屬性。

    只有被AjaxMethodAttribute標記的屬性才能被外部調用。

  3.優化反射。

    利用緩存,避免每次都根據函數名稱去搜索函數信息。

  標記屬性:

    public class AjaxMethodAttribute : Attribute
    {
    }

  緩存對象:  

    public class CacheMethodInfo
    {
        public string MethodName { get; set; }
        public MethodInfo MethodInfo { get; set; }
        public ParameterInfo[] Parameters { get; set; }
    }

  基類代碼:

     public class PageBase : Page
    {
        private static Hashtable _ajaxTable = Hashtable.Synchronized(new Hashtable());

        public override void ProcessRequest(HttpContext context)
        {            
            HttpRequest request = context.Request;
            if (string.Compare(request.Headers["AjaxFlag"],"XHR",true) == 0)
            {
                InvokeMethod(request.Headers["MethodName"]);
            }
            base.ProcessRequest(context);
        }

        /// <summary>
        /// 反射執行函數
        /// </summary>
        /// <param name="methodName"></param>
        private void InvokeMethod(string methodName)
        {
            if (string.IsNullOrEmpty(methodName))
            {
                EndRequest("MethodName標記不能爲空!");
            }
            CacheMethodInfo targetInfo = TryGetMethodInfo(methodName);
            if (targetInfo == null)
            {
                EndRequest("找不到合適的方法調用!");
            }
            try
            {
                object[] parameters = GetParameters(targetInfo.Parameters);
                string data = targetInfo.MethodInfo.Invoke(this, parameters) as string;
                EndRequest(data);
            }
            catch (FormatException)
            {
                EndRequest("參數類型匹配發生錯誤!");
            }
            catch (InvalidCastException)
            {
                EndRequest("參數類型轉換髮生錯誤!");
            }
            catch (ThreadAbortException)
            {
            }
            catch (Exception e)
            {
                EndRequest(e.Message);
            }
        }

        /// <summary>
        /// 獲取函數元數據並緩存
        /// </summary>
        /// <param name="methodName"></param>
        /// <returns></returns>
        private CacheMethodInfo TryGetMethodInfo(string methodName)
        {
            Type type = this.GetType().BaseType;
            string cacheKey = type.AssemblyQualifiedName;
            Dictionary<string, CacheMethodInfo> dic = _ajaxTable[cacheKey] as Dictionary<string, CacheMethodInfo>;
            if (dic == null)
            {
                dic = new Dictionary<string, CacheMethodInfo>();
                MethodInfo[] methodInfos = (from m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
                                            let ma = m.GetCustomAttributes(typeof(AjaxMethodAttribute), false)
                                            where ma.Length > 0
                                            select m).ToArray();

                foreach (var mi in methodInfos)
                {
                    CacheMethodInfo cacheInfo = new CacheMethodInfo();
                    cacheInfo.MethodName = mi.Name;
                    cacheInfo.MethodInfo = mi;
                    cacheInfo.Parameters = mi.GetParameters();
                    dic.Add(mi.Name, cacheInfo);
                }
                _ajaxTable.Add(cacheKey, dic);
            }
            CacheMethodInfo targetInfo = null;
            dic.TryGetValue(methodName, out targetInfo);
            return targetInfo;
        }

        /// <summary>
        /// 獲取函數參數
        /// </summary>
        /// <param name="parameterInfos"></param>
        /// <returns></returns>
        private object[] GetParameters(ParameterInfo[] parameterInfos)
        {
            if (parameterInfos == null || parameterInfos.Length <= 0)
            {
                return null;
            }
            HttpRequest request = this.Context.Request;
            NameValueCollection nvc = null;
            string requestType = request.RequestType;
            if (string.Compare("GET", requestType, true) == 0)
            {
                nvc = request.QueryString;
            }
            else
            {
                nvc = request.Form;
            }
            int length = parameterInfos.Length;
            object[] parameters = new object[length];
            if (nvc == null || nvc.Count <= 0)
            {
                return parameters;
            }
            for (int i = 0; i < length; i++)
            {
                ParameterInfo pi = parameterInfos[i];
                string[] values = nvc.GetValues(pi.Name);
                object value = null;
                if (values != null)
                {
                    if (values.Length > 1)
                    {
                        value = String.Join(",", values);
                    }
                    else
                    {
                        value = values[0];
                    }
                }
                if (value == null)
                {
                    continue;
                }
                parameters[i] = Convert.ChangeType(value, pi.ParameterType);
            }            
            return parameters;
        }

        private void EndRequest(string msg)
        {
            HttpResponse response = this.Context.Response;
            response.Write(msg);
            response.End();
        }
    }

  頁面類:

        [AjaxMethod]
        public string GetData3(int i, double d, string str)
        {
            string[] datas = new string[] { i.ToString(), d.ToString(), str };
            return "參數分別是:" + String.Join(",", datas);
        }    

  前臺代碼:

    function getData3(){
        $.ajax({
            headers:{"AjaxFlag":"XHR","MethodName":"GetData3"},
            data:{"i":1,"d":"10.1a","str":"hehe"},
            success:function(data){
                $("#result").text(data);
            }
        });
    }

5、總結

  上面的頁面基類已經具有能夠完成基本的功能,但它還不夠好。主要有:

  1. 依附在頁面基類。對於原本有頁面基類的,無疑會變得更加複雜。咱們但願把它獨立開來,變成一個單獨的組件。

  2. 效率問題。反射的效率是很低的,尤爲在web這類應用程序上,更應該慎用。以動態執行函數爲例,效率主要低在:a.根據字符串動態查找函數的過程。b.執行函數時,反射內部須要將參數打包成一個數組,再將參數解析到線程棧上;在調用前CLR還要檢測參數的正確性,再判斷有沒有權限執行。上面的優化其實只優化了一半,也就是優化了查找的過程,而Invoke一樣會有性能損失。固然,隨着.net版本越高,反射的效率也會有所提高,但這種動態的東西,始終是用效率換取靈活性的。

  3.不能支持複雜參數。有時候參數比較多,函數參數通常會封裝成一個對象類型。

  4. AjaxMethodAttribute只是一個空的標記屬性。咱們能夠爲它加入一些功能,例如,標記函數的名稱、是否使用Session、緩存設置等均可以再這裏完成。

  用過WebForm的朋友可能會提到AjaxPro組件,這是一個開源的組件,下一篇就經過源碼瞭解這個組件,借鑑它的處理過程,而且分析它的優缺點。

相關文章
相關標籤/搜索