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

结语

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

你可能感兴趣的:(Web项目演化系列--路由解析)