开发Web应用程序的时候,在页面上总会放置大量的连接,而连接的生成方式看似简单,也有许多不一样的变化,且各有利弊。如今咱们就来看看,在一个ASP.NET MVC应用程序的视图中若是要生成一个连接地址又有哪些作法,它们之间又是如何演变的。
目标
做为示例,咱们总要有个目标URL。咱们这里的目标为面向以下Action的URL,也就是一篇文章的详细页:
public class ArticleController : Controller
{
public ActionResult Detail(Article article)
{
...
}
}
public class Article
{
public int ArticleID { get; set; }
public string Title { get; set; }
}
而咱们的目标URL则是文章的ID与标题的联合,其中标题里的空格替换为很短横线——把文章的标题放入URL天然是为了SEO。因而乎咱们使用这样的Route规则:
routes.MapRoute(
"Article.Detail", // Route name
"article/{article}", // URL with parameters
new { controller = "Article", action = "Detail" } // Parameter defaults
);
在URL Routing捕获到article以后,它的形式多是这样的:
10-this-is-the-title
咱们只考虑这个ID,后面的字符串虽然在URL中,可是彻底被忽略。在实际项目中,咱们能够编写一个Model Binder从这样一个字符串中提取ID,再获取对应的Article对象。不过咱们的如今不关注这个。
咱们的目标只有一个:如何生成URL。
方法一:直接拼接字符串
这是个最直接,最容易想到的作法:
<% foreach (var article in Models.Articles) { %>
<a href="/article/<%= article.ArticleID %>-<%= Url.Encode(article.Title.Replace(' ', '-')) %>">
<%= Html.Encode(article.Title) %>
</a>
<% } %>
这个作法随着ASP.NET的诞生陪伴咱们一路走来,已经有七、8个年头了,相信大部分朋友对它都不会陌生。它的优势天然是最为简单,最为直接,几乎没有任何门槛,也不须要任何准备就能够直接使用,并且理论上性能也是最佳的。可是它的缺点也很明显,那就是须要在每一个页面,每一个地方都重复这样一个字符串。试想,若是咱们URL的生成规则突然有所变化,又会怎么样?咱们必须找出全部的生成连接的地方,一个一个改过来。这每每是一个浩大的工程,并且很是容易出错——由于咱们根本没有静态检查能够依托。所以,在实际状况下,除非是快速开发的超小型的,随作随抛的实验性项目,通常不建议使用这样的作法。
方法二:使用辅助方法
为了不方法一的缺点,咱们可使用一个辅助方法来生成URL:
public static class ArticleUrlExtensions
{
public static string ToArticle(this UrlHelper helper, Article article)
{
return "/article/" + article.ArticleID + "-" + helper.Encode(article.Title.Replace(' ', '-'));
}
}
咱们把负责生成URL的辅助方法写做UrlHelper的扩展方法,因而咱们就能够在页面上这样生成URL了(省略多余标记):
<a href="<%= Url.ToArticle(article) %>">...</a>
这个作法的优势在于把生成URL的逻辑聚集到了一处,这样若是须要变化的时候只须要修改这一个地方就好了,并且它几乎没有任何反作用,使用起来也很是简单。而它的缺点仍是在于有些重复:若是这个URL修改涉及到Route配置的变化(例如从http://www.domain.com/article/5变成了http://articles.domain.com/5),则ToArticle方法也必须随之修改。也就是说,这个方法对DRY(Don’t Repeat Yourself)原则贯彻地还不够完全。不过,对于通常项目来讲,这个作法也不会有太大问题——这也是构造URL方式的底线。
方法三:从Route配置中生成URL
对于URL Routing的双向职责我已经提过无数次了,咱们配置了Route规则,那么即可以使用它来生成URL:
public static string ToDetail(this UrlHelper helper, Article article)
{
var values = new
{
article = article.ArticleID + "-" + helper.Encode(article.Title.Replace(' ', '-'))
};
var path = helper.RouteCollection.GetVirtualPath(
helper.RequestContext, "Article.Detail", new RouteValueDictionary(values));
return path.VirtualPath;
}
因为Route配置知道如何根据Route Value集合里的值生成一个URL,所以咱们只要把这个职责交给它便可。通常来讲,咱们会指定Route规则的名称,这样节省了遍历尝试每一个规则的开销,也不会被冲突问题所困扰。此时,即使是URL须要变化,只要调整Route规则便可——只要保持规则对“值”的需求不变就好了。例如以前提到的URL的变化,咱们只要把Route配置调整为:
routes.MapDomain(
"Article",
"http://articles.{*domain}",
innerRoutes =>
{
innerRoutes.MapRoute(
"Detail",
"",
new { controller = "Article", action = "Detail" });
};
这个作法的优势在于“自动”与Route配置同步,几乎不须要额外的逻辑。而它的缺点——可能在从性能角度上考虑会有“细微”的差距(在实际应用中是否重要另当别论)……
方法四:使用Lambda表达式生成URL
我也常常强调使用Lambda表达式生成URL的好处:
<a href="<%= Url.Action<ArticleController>(c => c.Detail(article)) %>">...</a>
因为在ASP.NET MVC中,一个URL的最终目标归根究竟是一个Action,所以若是咱们能够更直观地在代码中表现出这一点,则能够进一步提升代码的可读性。这一点在ASP.NET MVC 1.0自带的MvcFutures项目中已经有所体现,只惋惜它做的远远不够,几乎没有任何实用价值。不过如今您也可使用MvcPatch项目进行开发,它提供了完整的使用Lambda表达式生成URL的能力,它相对于MvcFutures里的辅助方法做了各类补充:
支持ActionNameAttribute
提升性能
容许忽略部分参数
可指定Route规则的名称
支持Action复杂参数的双向转化
使用这种方式,咱们须要对Action方法作些简单的修改:
public class ArticleBinder : IModelBinder, IRouteBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
...
}
public RouteValueDictionary BindRoute(RequestContext requestContext, RouteBindingContext bindingContext)
{
var article = (Article)bindingContext.Model;
var text = article.ArticleID + "-" + HttpUtility.UrlEncode(article.Title.Replace(' ', '-'));
return new RouteValueDictionary(new { bindingContext.ModelName = text });
}
}
public class ArticleController : Controller
{
[RouteName("Article.Detail")]
public ActionResult Detail([ModelBinder(typeof(ArticleBinder))]Article article)
{
...
}
}
请注意咱们对Action方法标记了RouteNameAttribute,以此指定Route规则的名称(第4点);同时,ArticleBinder也实现一个新的接口IRouteBinder负责从Article对象转化为Route Value。
这个作法的优势在于基本上回避了“生成URL”这个工做,而将关注点放在Action方法这个根本的目标上。此外,各类逻辑也很内聚,它们都环绕在Action方法周围,遇到问题也不用四散查询,而将Article对象转化为Route Value的职责也和它的对应操做放在了一块儿,更容易进行独立的单元测试。此外,使用Lambda表达式生成URL还能得到编译器的静态检查,这确保了能够在编译期间解决尽量多的问题。
而它的缺点主要是比较复杂,若是您不使用MvcPatch项目的话,可能就须要自行补充许多辅助方法,它们并不那么简单。此外,在视图上的代码页稍显多了一些。还有即是基于表达式树解析的作法多少会有些性能损失,咱们下次再来关注这个问题。
方法五:简化Lambda表达式的使用
第五个方法实际上是前者的补充。例如,咱们能够再准备这样一个辅助方法:
public static class ArticleUrlExtensions
{
public static string ToArticle(this UrlHelper urlHelper, Expression<Action<ArticleController>> action)
{
return urlHelper.Action<ArticleController>(action);
}
}
这样在页面上使用时无须指定ArticleController类了——这类名的确有些长:
<a href="<%= Url.ToArticle(c => c.Detail(article)) %>">...</a>
或者,咱们能够结合方法二或三,提供一个额外的辅助方法:
public static class ArticleUrlExtensions
{
public static string ToArticle(this UrlHelper urlHelper, Article article)
{
return urlHelper.Action<ArticleController>(c => c.Detail(article));
}
}
至于最终使用哪一个辅助方法,我想问题都不是很大。前者的“准备工做”更为简单,只需为每一个Controller准备一个辅助方法就够了,然后者则须要为每一个Action提供一个辅助方法,不过它使用起来却更为方便一些。
这个作法的优势在于继承了Lambda表达式构造URL的优点以外,还简化了它的使用。至于缺点,可能也和Lambda表达式相似吧,例如准备工做较多,性能理论上略差一些。
第五个方法,也是我在ASP.NET MVC项目中使用的“标准作法”。
总结
此次咱们把“URL生成”这个简单的目标使用各类方法“演变”了一番,您能够选择地使用。这个演变的过程,其实也是一步步发现缺点,再进行针对性改进的过程。咱们虽然使用在ASP.NET MVC的视图做为演示载体,可是它的方式和思路并不只限于此,它也能够用在ASP.NET MVC的其它方面(如在Controller中生成URL),或是其它模型(如WebForms),甚至与Web开发并没有关联的应用程序开发上面。