Hello World
Spiga

浅谈URL生成方式的演变

2009-10-29 00:29 by 老赵, 19724 visits

开发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里的辅助方法作了各种补充:

  1. 支持ActionNameAttribute
  2. 提高性能
  3. 允许忽略部分参数
  4. 可指定Route规则的名称
  5. 支持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开发并无关联的应用程序开发上面。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

19 条回复

  1. 夕颜
    *.*.*.*
    链接

    夕颜 2009-10-29 00:38:00

    哦~soga

  2. 勇赴
    *.*.*.*
    链接

    勇赴 2009-10-29 07:24:00

    板凳,不容易,哈哈

  3. 园子里的鸟[未注册用户]
    *.*.*.*
    链接

    园子里的鸟[未注册用户] 2009-10-29 08:03:00

    老赵很少写系列专题之类的东西,一般都是随笔啊, 呵呵。

  4. LLgogogo
    *.*.*.*
    链接

    LLgogogo 2009-10-29 08:14:00

    MVC还是不熟

  5. 老赵
    admin
    链接

    老赵 2009-10-29 08:33:00

    @园子里的鸟
    是啊,想到哪里写到哪里。

  6. 假正经哥哥
    *.*.*.*
    链接

    假正经哥哥 2009-10-29 08:59:00

    这么做是否就是避免Url.Action("Controller","Action")的拼写错误?

  7. 老赵
    admin
    链接

    老赵 2009-10-29 09:02:00

    假正经哥哥:这么做是否就是避免Url.Action("Controller","Action")的拼写错误?


    可以避免。

  8. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-10-29 09:12:00

    夕颜:哦~soga


    您是巴嘎人吗?

  9. 假正经哥哥
    *.*.*.*
    链接

    假正经哥哥 2009-10-29 09:46:00

    Jeffrey Zhao:

    假正经哥哥:这么做是否就是避免Url.Action("Controller","Action")的拼写错误?


    可以避免。


    我的意思是:只是这个作用?

  10. 老赵
    admin
    链接

    老赵 2009-10-29 09:59:00

    @假正经哥哥
    好处都在文章里写着,避免拼写错误只是利用了“静态检查”这个优势而已。

  11. 横刀天笑
    *.*.*.*
    链接

    横刀天笑 2009-10-29 11:11:00

    Jeffrey Zhao:
    @园子里的鸟
    是啊,想到哪里写到哪里。


    呵呵,那你以后也出一本书叫做:《软件开发沉思录---老赵文集》

  12. 老赵
    admin
    链接

    老赵 2009-10-29 11:47:00

    @横刀天笑
    俺最多随想,不会沉思滴……

  13. Leon Weng
    *.*.*.*
    链接

    Leon Weng 2009-10-29 12:47:00

    老赵,能否写一些WEBFORM方面的分享一下呢?

  14. 小风(wind)
    *.*.*.*
    链接

    小风(wind) 2009-10-29 17:54:00

    赵老师,您好
    赵老师能不能讲解一下实战项目呢。
    说实话学一些知识我们要应用才行这样才能掌握的很牢固和理解更透彻
    现在园子里有许多都是新手,缺乏项目经验,如果您有时间的话,我希望您能分享你一下的项目经验,和一些编程思想。在编程中给我们讲解一些新的知识,谢谢

  15. 老赵
    admin
    链接

    老赵 2009-10-29 18:53:00

    @小风(wind)
    我难道不是一直在做这个事情嘛……

  16. dark
    *.*.*.*
    链接

    dark 2009-10-29 19:14:00

    Jeffrey Zhao:
    @小风(wind)
    我难道不是一直在做这个事情嘛……


    生活就是每天看看首页和老赵的文章 然后好的保存下来 细读...
    最近在浩方的重庆区打魔兽争霸 发现那里的人对话蛮有意思 比如:
    环境:澄海5.56CREG 不带复活版
    打到3分之一时.....
    A君:我们这边有个人还问为什么买不了复活(他都点半天复活了)..
    B君:你告诉他嘛.
    C君:对嘛,你就告诉他嘛.....
    PS:看到那个"嘛"的联想.......

  17. 海洋——海纳百川,有容乃大.
    *.*.*.*
    链接

    海洋——海纳百川,有容乃大. 2009-11-02 18:35:00

    还在看。

  18. lguyss
    218.58.70.*
    链接

    lguyss 2010-10-20 17:30:02

    (var post in Model.Posts)报错 Url.ToPostByRaw(Model.Blog, post)的Url.ToPostByRaw是怎么引用起来的?

  19. 小鸟
    113.111.159.*
    链接

    小鸟 2010-12-02 22:28:25

    老赵,有个问题想请教一下你哦.

    项目中用的ASP.NET MVC,做到后面要做一个功能,主要用来给编辑人员编辑页面.以后就需要程序员动手修改了.

    开始规划了一下,其实也就修改View吧.把一些页面模块封装成是一个个插件,有点类似.net控件,编辑人员可以拖到编辑器里设置相关属性什么的..

    理想状态下,添加页面的也可以交给编辑人员来做的.

    可是MVC中的一个页面对应着一个Action,如果添加页面,也要添加相应的Action,需要重新编译,这样做也就不合理了...

    请问想在ASP.NET MVC环境中实现这样的功能有什么好的方法吗?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我