浅谈URL生成方式的演变
2009-10-29 00:29 Jeffrey Zhao 阅读(21135) 评论(18) 编辑 收藏 举报开发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的诞生陪伴我们一路走来,已经有7、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里的辅助方法作了各种补充:
使用这种方式,我们需要对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开发并无关联的应用程序开发上面。