编写轻量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的名称即可。
前台代码:
1 2 3 4 5 6 7 8 9 10 11 12 | < 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 > |
后台代码:
1 2 3 4 5 6 7 8 9 10 11 | 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">标签。
前台代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | < 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 > |
后台代码:
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 | 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,可以通过加载用户控件生成。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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类型的就可以被外部调用,也可以通过标记属性标记。
通过上面的分析,简单实现如下
页面基类:
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 | 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(); } } |
页面类:
1 2 3 4 5 6 7 8 9 10 11 12 | public partial class Test1 : PageBase { protected void Page_Load( object sender, EventArgs e) { } public string GetData() { return "213" ; } } |
前台代码:
1 2 3 4 5 6 7 8 | function getData(){ $.ajax({ headers:{ "AjaxFlag" : "XHR" , "MethodName" : "GetData" }, success: function (data){ $( "#result" ).text(data); } }); } |
四、优化版页面基类
上面的页面基类功能很少,而且通过反射这样调用的效率很低。这里优化一下:
1.可以支持简单类型的参数。
例如上面的GetData可以是:GetData(string name),通过函数元数据可以获取相关的参数,再根据请求的参数,就可以设置参数了。
2.加入标记属性。
只有被AjaxMethodAttribute标记的属性才能被外部调用。
3.优化反射。
利用缓存,避免每次都根据函数名称去搜索函数信息。
标记属性:
1 2 3 | public class AjaxMethodAttribute : Attribute { } |
缓存对象:
1 2 3 4 5 6 | public class CacheMethodInfo { public string MethodName { get ; set ; } public MethodInfo MethodInfo { get ; set ; } public ParameterInfo[] Parameters { get ; set ; } } |
基类代码:
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 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(); } } |
页面类:
1 2 3 4 5 6 | [AjaxMethod] public string GetData3( int i, double d, string str) { string [] datas = new string [] { i.ToString(), d.ToString(), str }; return "参数分别是:" + String.Join( "," , datas); } |
前台代码:
1 2 3 4 5 6 7 8 9 | 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组件,这是一个开源的组件,下一篇就通过源码了解这个组件,借鉴它的处理过程,并且分析它的优缺点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?