编写轻量ajax组件01-与webform平台上的各种实现方式比较
前言
asp.net webform 和 asp.net mvc(简称mvc) 都是基于asp.net的web开发框架,两者有很大的区别,其中一个就是mvc更加注重http本质,而webform试图屏蔽http,为此提供了大量的服务器控件和viewstate机制,让开发人员可以像开发windows form应用程序一样,基于事件模型去编程。两者各有优缺点和适用情景,但mvc现在是许多asp.net开发者的首选。
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开始的设计就将服务器控件作为主要组成部分,如果不使用它,那么只能利用它的扩展性去实现。
本系列就是实现一个基于webform平台的轻量级ajax组件,主要分为三个部分:
1. 介绍webform下各种实现方式。
2. 分析ajaxpro组件。
3. 编写自己的ajax组件。
一、ajax简介
异步允许我们在不刷新整个页面的情况下,像服务器请求或提交数据。对于复杂的页面,为了请求一点数据而重载整个页面显然是很低效的,ajax就是为了解决这个问题的。ajax的核心是xmlhttprequest对象,通过该对象,以文本的形式向服务器提交请求。xmlhttprequest2.0后,还支持提交二进制数据。
ajax安全:出于安全考虑,ajax受同源策略限制;也就是只能访问同一个域、同一个端口的请求,跨域请求会被拒绝。当然有时候需求需要跨域发送请求,常用的跨域处理方法有cors(跨域资源共享)和jsonp(参数式json)。
ajax数据交互格式:虽然ajax核心对象xmlhttprequest有"xml"字眼,但客户端与服务器数据交换格式不局限于xml,例如现在更多是使用json格式。
ajax 也是有缺点的。例如对搜索引擎的支持不太好;有时候也会违背url资源定位的初衷。
二、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(); } }
三、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); } }); }
四、优化版页面基类
上面的页面基类功能很少,而且通过反射这样调用的效率很低。这里优化一下:
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(); } }
页面类:
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); } }); }
五、总结
上面的页面基类已经具备可以完成基本的功能,但它还不够好。主要有:
1. 依附在页面基类。对于本来有页面基类的,无疑会变得更加复杂。我们希望把它独立开来,变成一个单独的组件。
2. 效率问题。反射的效率是很低的,尤其在web这类应用程序上,更应该慎用。以动态执行函数为例,效率主要低在:a.根据字符串动态查找函数的过程。b.执行函数时,反射内部需要将参数打包成一个数组,再将参数解析到线程栈上;在调用前clr还要检测参数的正确性,再判断有没有权限执行。上面的优化其实只优化了一半,也就是优化了查找的过程,而invoke同样会有性能损失。当然,随着.net版本越高,反射的效率也会有所提升,但这种动态的东西,始终是用效率换取灵活性的。
3.不能支持复杂参数。有时候参数比较多,函数参数一般会封装成一个对象类型。
4. ajaxmethodattribute只是一个空的标记属性。我们可以为它加入一些功能,例如,标记函数的名称、是否使用session、缓存设置等都可以再这里完成。
用过webform的朋友可能会提到ajaxpro组件,这是一个开源的组件,下一篇就通过源码了解这个组件,借鉴它的处理过程,并且分析它的优缺点。
上一篇: 谈谈Ajax原理实现过程