搭建一个web项目不可能一蹴而就,会先从最基础的开始,不考虑数据库的支持,不考虑任何业务逻辑,使其以最简单的方式运行起来,然后慢慢的填充各个部分,使其从一个单机项目发展到集群分布式的大型项目,而其中最基础的部分便是路由了。
之前已经写过一篇关于web form支持mvc的文章了,该文章为:《 web form中自定义HttpHandler仿mvc 》。
此篇文章会在之前文章的基础上进行重构,抽取一些公用的代码,其实在开发过程当中,公用代码是很重要的一部分,它既包括了通用的代码,例如:利用表达式树实现反射、从Http请求中获取请求对象、加密解密等,也包括了一些业务公用代码,例如:根据路由访问view文件夹下相应的aspx或ascx、根据规则生成相应的前端验证组件、前端的复合组件等,在不断的开发、重构过程当中,就可以慢慢形成自己的核心代码库了,这对于开发是非常有利的,虽然其中会涉及到版本问题,会是很多开发人员萌生怯意,但是只要处理的当,好处还是大于坏处的。
重构
首先解析路由的大致代码如下:
var match = URL_RULE.Match(context.Request.AppRelativeCurrentExecutionFilePath); if (!match.Success) { //404 return; } var controller = Assembly.Load("业务层程序集").CreateInstance("Controller完整程序集" + match.Groups[1].Value, true); if (controller == null) { //404 return; } var method = controller.GetType().GetMethod(match.Groups[2].Value, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); if (method == null) { //404 return; } object[] methodArgs; if (method.GetParameters().Length > 0) { //从HttpRequest中获取方法参数 } else { methodArgs = new object[] { }; } try { method.Invoke(controller, methodArgs); } catch { //error }
接下来要考虑的就是如何在其他的项目中重用以上的代码,通过观察,不同项目中有所差别的主要有以下几个部分:
1、路由解析规则(正则)
2、404处理
3、Error处理
那么可以开放类似如下的接口,提供给其他的项目实现,并且将MvcHandler转移到公用库中去,代码调整如下:
//配置接口 public interface IMvcSetting { string ControllerAssemblyString { get; } string ControllerNamespaceFormat { get; } Regex UrlRule { get; } ActionResult Page404 { get; } ActionResult PageError { get; } } //mvcHandler private static IMvcSetting s_Setting; public static void Init(IMvcSetting setting) { s_Setting = setting; } public void ProcessRequest(HttpContext context) { var match = s_Setting.UrlRule.Match(context.Request.AppRelativeCurrentExecutionFilePath); if (!match.Success) { s_Setting.PageError.ExecuteResult(context); return; } var typeName = string.Format(s_Setting.ControllerNamespaceFormat, s_Setting.ControllerAssemblyString, match.Groups[1].Value); var controller = Assembly.Load(s_Setting.ControllerAssemblyString).CreateInstance(typeName, true); if (controller == null) { s_Setting.Page404.ExecuteResult(context); return; } var method = controller.GetType().GetMethod(match.Groups[2].Value, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); if (method == null) { s_Setting.Page404.ExecuteResult(context); return; } object[] methodArgs; if (method.GetParameters().Length > 0) { //从HttpRequest中获取方法参数 } else { methodArgs = new object[] { }; } try { (method.Invoke(controller, methodArgs) as ActionResult).ExecuteResult(context); } catch { s_Setting.PageError.ExecuteResult(context); } }
有了公用库后,其他项目就可以直接引用该MvcHandler,并实现相应的IMvcSetting,然后在Global.asax中调用MvcHandler.Init来进行设置。
请求参数的话,不管是QueryString还是Params都是NameValueCollection,可以在公用库为NameValueCollection增加一个扩展方法,根据ParameterInfo[]获取请求数据,代码就不提供了。
但是当方法的参数无法从QueryString或者Params中获取时,就比较麻烦了,因此如果将方法中的参数转换成一个对象,代码如下:
public class LoingRequest { public string Name { get; set; } public string Password { get; set; } } public class UserController { public ActionResult Login(LoginRequest req) { //coding } }
那么只要将扩展方法修改为转化成对象,代码大致如下:
public static class NameValueCollectionExtension { public static object ExtractFor(this NameValueCollection collection, string key, Type valueType) { try { if (valueType == typeof(Guid)) { return new Guid(collection[key]); } else if (valueType.IsEnum) { return Enum.Parse(valueType, collection[key]); } else if (valueType.IsClass && valueType != typeof(string)) { return collection.ExtractFor(valueType); } else if (valueType.IsValueType && valueType.IsGenericType) { string value = collection[key]; if (string.IsNullOrEmpty(value)) return null; return collection.ExtractFor(key, valueType.GetGenericArguments()[0]); } return Convert.ChangeType(collection[key], valueType); } catch { return valueType != typeof(string) ? Activator.CreateInstance(valueType) : null; } } public static object ExtractFor(this NameValueCollection collection, Type valueType) { try { var obj = Activator.CreateInstance(valueType); foreach (var property in valueType.GetProperties( BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty)) { if (property.PropertyType.IsClass && property.PropertyType != typeof(string)) continue; property.SetValue(obj, collection.ExtractFor(property.Name, property.PropertyType), null); } return obj; } catch { return Activator.CreateInstance(valueType); } } } //mvcHandler object[] methodArgs; if (method.GetParameters().Length > 0) { var reqArg = context.Request.QueryString.ExtractFor(method.GetParameters()[0].ParameterType); methodArgs = new object[]{ reqArg }; }
以上路由方法获取参数对象并不考虑是get或者post,真实环境中肯定不可能都是get方法的,因此路由方法需要一个标识来判断,这时候就轮到Attribute出场了。
有了Attribute不仅可以标识Post、Get,也可以用来标识是否需要认证等,此处开发人员不要考虑路由方法既可以post访问,又可以get访问,这样只会增加判断的复杂度,当方法需要重用的时候,只要将内容重构成一个公用的方法,然后让post、get方法都调用它既可,代码调整如下:
if (method.GetParameters().Length > 0) { var isPost = method.GetCustomAttributes(typeof(HttpPostAttribute), false).Length > 0; var nameValues = isPost ? context.Request.Params : context.Request.QueryString; var reqArg = nameValues.ExtractFor(method.GetParameters()[0].ParameterType); if (isPost) { //设置文件类型的属性 } }
结语
对于路由的重构就差不多结束了,由于本人的表达能力不好,因此代码占大部分,如果以上有错误,请各位给我留言,我会尽快修正,谢谢。