当前位置: 移动技术网 > IT编程>脚本编程>Ajax > 编写轻量ajax组件02--浅析AjaxPro

编写轻量ajax组件02--浅析AjaxPro

2017年12月12日  | 移动技术网IT编程  | 我要评论

前言

  介绍了在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)); 用来注册所需要的脚本。我们注意到在前台页面引入了如下脚本:

也就是每个页面都会都会发起这几个请求。这几个都是.ashx结尾的文件,但实际里面都是js代码;这些js有的是作为资源嵌套在dll内部,有的是自动生成的,主要是封装了ajax请求相关方法,以及让我们可以用:名称空间.页面类名称.标记方法名称 这样去调用方法。为什么要用.ashx而不是用.js呢?因为作为组件内部的资源文件,外部无法直接请求.js文件,而.ashx可以被拦截,然后用response.write将内容输出。

  如果每次都生成和发送这些脚本的效率是很低的,ajaxpro内部的处理是判断请求头的if-none-math和if-modified-since,如果两个都和缓存的一样,就返回一个304状态码。所以,客户端只有首次请求服务端会返回文件的内容,后续的都只返回304表示使用本地缓存。我们刷新页面可以验证这个过程:

  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请求也要符合这个格式。如:,这个格式由前台脚本自动生成,并不需要我们去构造。仔细观察,会发现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组件的源代码,有兴趣的朋友可以研究研究。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网