淺談URL生成方式的演變

 Source: http://blog.zhaojie.me/2009/10/several-ways-of-generating-url.html html

 
開發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開發並沒有關聯的應用程序開發上面。
 
相關文章
相關標籤/搜索