转载

Web项目演化系列--路由解析

搭建一个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)  {   //设置文件类型的属性  } }

结语

对于路由的重构就差不多结束了,由于本人的表达能力不好,因此代码占大部分,如果以上有错误,请各位给我留言,我会尽快修正,谢谢。

正文到此结束
Loading...