zoukankan      html  css  js  c++  java
  • 编写轻量ajax组件02-AjaxPro浅析

    前言

      上一篇介绍了在webform平台实现ajax的一些方式,并且实现一个基类。这一篇我们来看一个开源的组件:ajaxpro。虽然这是一个比较老的组件,不过实现思想和源码还是值得我们学习的。通过上一篇的介绍,我们知道要调用页面对象的方法,就是靠反射来实现的,关键是整个处理过程,包括反射调用方法、参数映射等。ajaxpro不仅在后台帮我们实现了这个过程,在前台也封装了请求调用的方法,例如ajax的相关方法,用ajaxpro的方法就可以发送异步请求了,不需要自己封装js或者使用js库。接下来就对这个组件进行浅析。

    一、ajaxpro的使用

      我们先来看这个组件如何使用。

      1. 注册AjaxHandlerFactory

      在web.config里进行如下配置:

        <httpHandlers>
          <add verb="POST,GET" path="ajaxpro/*.ashx" type="AjaxPro.AjaxHandlerFactory, AjaxPro"/>
        </httpHandlers>
    

      简单的说,请求的url符合 ajaxpro/*.ashx 格式的,都会被AjaxHandlerFactory处理,这是一个实现IHandlerFactory接口的工厂类,用来获取IHandler处理程序。其中type的格式是:"名称控件.类名称,程序集名称"。

      2. 在页面类Page_Load事件进行注册

            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 也可以获得这个类型)

      3.用AjaxMethod标记方法  

            [AjaxMethod]
            public List<string> GetList(string input1,string input2)
            {
                return new List<string> { input1, input2 };
            }
    

      AjaxMethod是一个标记属性,表示这个方法用于处理ajax请求,它最终通过反射执行;它有几个构造函数对,对于有些需要缓存的数据,可以设置缓存时间;如果我们的请求不需要使用Session,可以设置HttpSessionStateRequirement;如果请求需要异步,例如请求一个耗时的web服务,也可以设置处理程序为异步状态。

      方法的返回值可以是简单的类型,也可以是复杂的类型;例如集合类型在前台获得就是一个数组。

      4.前台调用

      后台的配置和使用都非常简单,接下来我们看前台如何发起请求。

        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库的方法。

    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包括了失败的信息。

      注意,上面注释掉的部分是同步请求的做法,这往往不是我们想要的,我曾经就见过有人这样错误的使用。

    二、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的定义如下:

        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的主要代码如下:

            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。有了这些信息,就可以反射执行方法了。这里核心代码为:

            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){}
            }

    三、总结

      我们总结一下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组件的源代码,有兴趣的朋友可以研究研究。

  • 相关阅读:
    Maven 集成Tomcat插件
    dubbo 序列化 问题 属性值 丢失 ArrayList 解决
    docker 中安装 FastDFS 总结
    docker 从容器中拷文件到宿主机器中
    db2 相关命令
    Webphere WAS 启动
    CKEDITOR 4.6.X 版本 插件 弹出对话框 Dialog中 表格 Table 自定义样式Style 问题
    SpringMVC JSONP JSON支持
    CKEDITOR 3.4.2中 按钮事件中 动态改变图标和title 获取按钮
    git回退到远程某个版本
  • 原文地址:https://www.cnblogs.com/4littleProgrammer/p/4926559.html
Copyright © 2011-2022 走看看