路由 - ASP.NET MVC 4 系列

       軟件開發人員經常對一些細小的細節問題倍加關注,由其在考慮源代碼的質量和結構時更是如此。所以,當遇到大部分使用 ASP.NET 技術構建的站點,使用以下的 URL 地址時,可能會有些奇怪:正則表達式

http://example.com/albums/list.aspx?catid=17173&genreid=33723&page=3算法

       既然咱們對代碼倍加劇視,爲何不能一樣的重視 URL 呢?雖然它看上去並非那麼重要,但它倒是一種合法且普遍使用的 Web 用戶接口!服務器

理解 URL(Uniform Resource Locator)

       可用性專家力勸開發人員重視 URL,並指出高質量的 URL 應該知足如下幾點要求:便於記憶和拼寫、簡短、便於輸入、能夠反映出站點結構、「可破解的」,用戶能夠移除 URL 的末尾,進而到達更高層次的信息體系結構、持久,不能改變mvc

       按照傳統,在不少 Web 框架中(如 ASP、JSP、PHP、ASP.NET 等),URL 表明的是磁盤上的物理文件,例如上面的 URL 咱們能夠肯定站點的目錄結構中有一個 albums 文件夾,且還包含一個 List.aspx 文件。URL 和文件系統的這種對應關係,並不適用於大部分基於 MVC 的 Web 框架,這類框架應用不一樣的方法把 URL 映射到某個類的方法調用,而不是磁盤上的某個物理文件app

       URL 是統一資源定位符的首字母所寫,資源是一種抽象概念,既能夠指一個文件,也能夠指方法調用的結果或服務器上的一些其餘內容框架

       URI 表明統一資源標識符,從技術角度看,全部 URL 都是 URI。W3C 認爲 URL是一個非正式的概念,它經過表示自身的主要訪問機制來標識資源。而有專家提出另外一種見解:URI 是某資源的標識符,URL 則爲獲取該資源提供了具體的信息asp.net

路由概述

       ASP.NET MVC 框架中的路由主要有兩種用途:post

  1. 匹配傳入的請求,並把這些請求映射到控制器操做。
  2. 構造傳出的 URL,用來響應控制器中的操做。

       不少開發人員喜歡把路由與 URL 重寫進行對比。由於這兩種方法均可用於分離傳入 URL 和結束處理請求。此外,它們均可覺得搜索引擎優化(Search Engine Optimization,SEO)構建「漂亮的」URL。然而,它們也有很大的區別:URL 重寫關注的是將一個 URL 映射到另外一個 URL,例如常把舊的 URL 映射到新的 URL,與之相比,路由關注的則是如何將 URL 映射到資源性能

路由的定義

       每一個 ASP.NET MVC 程序都至少須要一個路由來定義本身處理請求的方式,但一般,老是會有一個或多個路由,很是複雜的程序可能會有數十個甚至更多。優化

       路由的定義是從 URL 模式開始的,由於它指定了與路由相匹配的模式。路由能夠指定它的 URL 及其默認值,能夠約束 URL 各個部分,提供關於路由如何、什麼時候與傳入的請求 URL 相匹配的嚴格控制

       如今清除 RegisterRoutes 方法中全部的代碼,而後添加一個很是簡單的路由,添加後以下:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("simple", "{first}/{second}/{third}");
}

       MapRoute 方法的最簡單形式是採用路由名稱和路由的 URL 模式。下表展現了在上面代碼中定義的路由如何把指定的 URL 解析成一個存儲在 RouteValueDictionary 實例中的鍵/值對,從而能夠幫助理解,路由如何把 URL 分解成稍後在請求管道中使用的重要信息片斷

URL

URL 參數值

/albums/display/123 first="albums"   second="display"   third="123"
/foo/bar/baz first="foo"   second="bar"   third="baz"
/a.b/c-d/e-f first="a.b"   second="c-d"   third="e-f"

       路由 URL 是由若干個 URL 段(斜槓之間全部內容)組成,每一個段都包括一組花括號限定的佔位符,這些佔位符就是 URL 參數這是一種模式匹配規則,用來決定路由是否適用於傳入的請求。針對本示例,因爲 URL 參數在默認的狀況下將匹配任何非空值,所以,示例中定義的規則能夠匹配任何帶有 3 個斷的 URL

       當客戶端的請求到達服務器時,路由解析請求的 URL,並將解析出的 路由參數值 放入字典(經過 RequestContext 訪問的 RouteValueDictionary)中,在生成的字典中把路由 URL 參數名稱做爲 key,將對應位置上的字段做爲 value

路由值

       若是真的請求上面註冊的 URL,會返回 404 錯誤。儘管可使用任何想要的名稱來定義路由,但 ASP.NET MVC 框架要求使用一些特定的參數名稱:{controller}、{action}

       {controller} 參數的值用於實例化一個控制器類,按照約定,ASP.NET MVC 把 Controller 後綴添加到 {controller} URL 參數值的後面構成一個類型名稱,而後根據該名稱查找實現了 System.Web.Mvc.IController 接口的類型,不區分大小寫

       {action} 參數值用來指明該類中須要調用的方法。

       如今,咱們將路由註冊代碼修改成 ASP.NET MVC 約定的模式:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("simple", "{controller}/{action}/{id}");
}

       參考上表第一個示例,如今變爲請求名稱爲「album」的 controller,框架把 Controller 做爲後綴添加到 URL 參數值「album」以後,從而獲得類型名稱「albumController」,不區分大小寫,且該類型若是還實現了 IController 接口,那麼該類就會被實例化,並用於處理這個請求

       注意:上表中第三個 URL 是一個有效的路由 URL,但它並不能匹配任何的控制器和操做,緣由很簡單,二者都不是有效的 ASP.NET 類名和方法名。

       除了 {controller} 和 {action} 以外,若是還有其餘任何路由參數,它們均可以做爲參數傳遞到操做方法中

       假設存在以下的控制器:

public class AlbumsController : Controller
{
    public ActionResult Display(int id)
    { 
        // do something...
        return View();
    }
}

       如今若是發出請求:/albums/display/123,上述代碼則徹底能被匹配。

       {controller}/{action}/{id} 中每個段都包含一個 URL 參數,同時 URL 參數也佔有對應的整個段。事實上,並不必定老是這樣,路由 URL 在段中也容許包含字面值,若是要把 MVC 集成到一個現有的站點中,而且想讓全部 MVC 請求都以 site 開頭,那能夠以下實現:

site/{controller}/{action}/{id} // 這個路由只有第一個段以 site 開頭,才能與請求匹配。

       還有更靈活的路由語法規則,在 URL 段中容許字面值和參數混合在一塊兒,僅有的限制是不容許兩個連續的 URL 參數

{language}-{country}/{controller}/{action} // 合法

{controller}.{action}.{id} // 合法

{controller}{action}/{id} // 錯誤的,路由沒法知道傳入請求 URL 的控制器部分什麼時候結束,操做方法部分什麼時候開始

       URL 模式及其匹配示例:

路由 URL 模式

匹配的 URL 示例

{controller}/{action}/{genre} /albums/list/rock
service/{action}-{format} /service/display-xml
{report}/{year}/{month}/{day} /sales/2010/06/19

路由默認值

       路由 URL 並非在匹配請求時所要考慮的惟一因素,還應該考慮爲路由 URL 參數提供的默認值。

       假設如今有一個沒有任何參數的操做方法:

public ActionResult List()
{
    // do something...
    return View();
}

       咱們會很天然的想到經過這樣的 URL 調用 List 方法:/albums/list,然而,根據先前定義的路由 URL 就不能正常運行,由於先前的路由定義只匹配包含 3 個段的 URL,但 /albums/list 只包含 2 個段。彷佛須要從新定義一個相似這樣的兩個段的路由:{controller}/{action}。但若是能指出先前的路由定義中,第三個段是可選的,不是更好?

       路由 API 容許爲參數段提供默認值,例如:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("simple", "{controller}/{action}/{id}", 
        new { id = UrlParameter.Optional });
}

      { id = UrlParameter.Optional } 爲 {id} 參數定義了默認值,該默認狀況就容許路由匹配沒有 id 參數的請求。換言之,該路由如今能夠匹配具備兩個段的 URL,也能夠匹配具備三個段的 URL!

       還能夠將 id 設置爲空串{id=""} 來實現上述功能,但爲何不呢?先前說過,框架會解析 URL 參數值,並將解析後的內容放入一個字典中,當使用 UrlParameter.Optional 時,在 URL 中並無提供值,路由就不會在字典中添加條目,若使用空串,則路由會在字典中添加 key 爲 id,值爲 空 的條目。某些場合中,這種差異是重要的,可讓咱們知道 id 值沒有被指定和指定爲空的區別。

       能夠爲多個參數提供默認值,下面的代碼爲 {action} 參數提供了一個默認值

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("simple", "{controller}/{action}/{id}",
        new { id = UrlParameter.Optional, action = "index" });
}

路由約束

       有時,相對於 URL 段的數量來講,還須要對 URL 有更多的控制,以下兩個 URL:

http://example.com/2008/01/23

http://example.com/posts/categories/aspnetmvc

       它們都包含 3 個段,且均可以和先前定義的默認路由相匹配。若是不當心,就會使系統查找一個名爲 2008Controller 的控制器和名爲 01 的方法,這顯然是很荒唐的。然而,僅經過查看這些 URL,咱們如何才能知道它們應該映射到哪些內容呢?

       約束容許 URL 段使用正則表達式來限制路由是否匹配請求,例如:

public static void RegisterRoutes(RouteCollection routes)
{
    // 映射指定的 URL 路由並設置默認路由值和約束。
    routes.MapRoute("blog", "{year}/{month}/{day}",
        new { controller = "blog", action = "index" },
        new { year = @"\d{4}", month = @"\d{2}", day = @"\d{2}" });
 
    routes.MapRoute("simple", "{controller}/{action}/{id}",
        new { id = UrlParameter.Optional, action = "index" });
}

       在路由的底層使用 Regex 類,熟悉正則表達式的語法規則,能夠知道 \d{4} 實際上匹配包含有 4 個連續數字的任意字符串,如「abc1234def」,然而,路由機制會總動使用「^」和「$」符號包裝指定的約束表達式,以確保表達式可以精確的匹配參數值。換言之,在這裏並不能匹配 「abc1234def」。

       這個路由添加在默認的 simple 路由以前,是由於路由會按前後順序與傳入的 URL 進行匹配,直到匹配成功。而 /2008/06/07 這類請求與兩個定義的路由都匹配,天然要把更具體的路由放在前面

路由命名

       ASP.NET 中的路由機制不要求路由具備名稱,且大多數狀況下沒有名稱的路由也能知足大多數應用場合。一般爲了生成一個 URL,只需抓取預約義的路由值,並把它們交給路由引擎,剩餘工做就由路由引擎來作。但有些狀況下,這種方法在選擇生成 URL 的路由時,會產生二義性,而爲路由指定名稱可解決這個問題,由於這樣能夠在生成 URL 時,對路由選擇進行精確控制

       假設應用程序已經定義瞭如下兩個路由:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute(
        name: "Test",
        url: "code/p/{action}/{id}",
        defaults: new { controller = "Section", action = "Index", id = "" }
    );
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = "" }
    );
}

       爲在視圖中生成一個指向每一個路由的超連接,編寫了下面兩行代碼:

@Html.RouteLink("Test", new { controller = "section", action = "Index", id = 123 })
@Html.RouteLink("Default", new { controller = "Home", action = "Index", id = 123 })

       注意,上面的兩個方法調用不能指定使用哪一個路由來生成連接。它們只是提供了一些路由值,來讓 ASP.NET 路由引擎幫助生成 URL。正如指望的那樣,生成了對應的 URL:

<a href="/code/p/Index/123">Test</a>
<a href="/Home/Index/123">Default</a>

       假設咱們在路由列表的開始部分添加了以下的路由,以便  /aspx/SomePage.aspx 頁面可以處理 URL/static/url:

routes.MapPageRoute("new", "static/url", "~/aspx/SomePage.aspx");

       將上面的路由移動到定義路由列表的開始位置,看起來是無足輕重的變化,但真的是這樣嗎?對於傳入的請求,該路由只能匹配 /static/url 的請求,這正是咱們想要的。可是如何生成 URL 呢?回到前面查看兩次調用 RouteLink 返回的結果,將會發現返回的兩個 URL 都是不可用的:

 
<a href="/static/url?controller=section&amp;action=Index&amp;id=123">Test</a>
<a href="/static/url?controller=Home&amp;action=Index&amp;id=123">Default</a>

       一般,當使用路由生成 URL 時,咱們提供的路由值會被用來填充本文開始所說的 URL 參數。因爲新的路由沒有 URL 參數,所以它能夠匹配每個可能生成的 URL,使其它已有的路由不可用

       這個問題修正起來很是簡單:生成 URL 時指定路由名稱。大多時候,路由機制挑選出來生成 URL 的路由徹底是隨機的,而一般咱們本身都很是明確本身想要的路由,所以,咱們能夠指定它。這不只能夠避免二義性,還能夠提升性能,由於路由引擎能夠直接定位到指定的路由。下面的代碼進行了修改,也獲得了正確生成的 URL:

@Html.RouteLink(
    linkText: "route: Test",
    routeName: "test",
    routeValues: new { controller = "section", action = "Index", id = 123 }
)
@Html.RouteLink(
    linkText: "route: Default",
    routeName: "default",
    routeValues: new { controller = "Home", action = "Index", id = 123 }
)
<a href="/code/p/Index/123">route: Test</a>
<a href="/Home/Index/123">route: Default</a>

段中的多個 URL 參數

       正如先前所述,路由 URL 的每一個段均可能含有多個參數,下面這些是有效 URL:

  • {title}-{artist}
  • Album{title}and{artist}
  • {filename}.{ext}

       爲了不二義性,咱們規定參數不能臨近,下面列出的 URL 都是無效的:

  • {title}{artist}
  • Download{filename}{ext}

       路由 URL 在與傳入的請求匹配時,它的字面值是與請求精確匹配的,而其中的 URL 參數則是貪婪匹配!這與正則表達式有一樣的含義,換言之,路由使每一個 URL 參數都儘量多的匹配文本

       例如,路由 {filename}.{ext} 是如何匹配 /asp.net.mvc.xml 請求的呢?若是 {filename} 不是貪婪匹配,那麼它只須要匹配 asp,而由 {ext} 參數匹配剩餘的 .net.mvc.xml,但因爲 URL 參數要求貪婪匹配,因此 {filename} 參數會盡量匹配它能匹配的文本 asp.net.mvc,但它不能再匹配更多的了,由於必須爲 .{ext} 部分留下匹配空間

揭祕路由如何生成 URL

       路由兩大主要職責,除了以前所敘述的如何匹配傳入的請求 URL以外,路由機制另外一大指責是構造與特定路由對應的 URL。在生成 URL 時,生成 URL 的請求應該首先與選擇用來生成 URL 的路由相匹配,這樣路由就能夠在處理傳入傳出 URL 時成爲一個完整的雙向系統!

       路由核心是一個很是簡單的算法,該算法基於一個由 RouteCollection 類和 RouteBase 類組成的簡單抽象對象。能夠採用多種方法來生成 URL,但這些方法都以調用一個 RouteCollection.GetVirtualPath 的重載方法而結束。該方法有兩個重載的版本:

public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
 
public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values);

       1. 路由集合經過 Route.GetVirtualPath 方法遍歷每一個路由並詢問:能夠生成給定參數的 URL 嗎?這個過程相似於路由在與傳入請求匹配時所運用的邏輯。

       2. 若是一個路由能夠應答,那麼它就返回一個包含了 URL 的 VirtualPathData 實例以及其餘匹配信息,不然它就返回空值,路由機制移向列表的下一個路由。

       重載版本二接收 3 個參數,多了路由名稱。在路由集合中路由名稱是惟一的,路由機制能夠當即找到指定名稱的路由,並進行上述邏輯,若指定的路由不能匹配指定的參數,Route.GetVirtualPath 返回空值,而且不會再匹配其餘路由。

URL 生成詳解

       Route 類提供了前面高層次算法的具體實現:

  1. 開發人員調用像 Html.ActionLink 或 Url.Action 之類的方法,這些方法反過來再調用 RouteCollection.GetVirtualPath 方法,並向它傳遞一個 RequestContext 對象、一個包含值的字典、選擇生成 URL 的路由名稱(可選參數)。
  2. 路由機制查看要求的路由 URL 參數(即沒有提供 URL 參數的默認值),並確保提供的路由值字典爲每個要求的參數提供一個值。不然,URL 生成程序會當即中止,並返回空值。
  3. 一些路由可能包含沒有對應 URL 參數的默認值。例如,路由可能爲 category 鍵提供一個默認值「pastries」,可是 category 不是路由 URL 的一個參數。這種狀況下,若是用戶傳入的路由值字典爲 category 提供了一個值,那麼該值必須匹配 category 的默認值!
  4. 路由系統應用路由的約束,若是有的話。
  5. 路由匹配成功。如今能夠查看每個 URL 參數,並嘗試用字典中的對應值填充對應參數,進而生成 URL。

       image

       image

溢出參數(overflow parameters)

       指在 URL 生成過程當中使用但沒有在路由定義中指定的路由值,且溢出參數會做爲查詢字符串參數附加在生成的 URL 以後。

       例以下面第一的默認路由:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = "" }
);

       若是使用這條指令渲染一個 URL:

@Url.RouteUrl(new { controller = "Report", action = "List", page = "123" })

       上述代碼生成的 URL 是:/Report/List?page=123

       假設定義了下面的路由:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapRoute("report", "reports/{year}/{month}/{day}", new { day = 1 });
}

       還有一些按照下面的通常格式,調用 Url.RouteUrl 方法後返回的結果:

@Url.RouteUrl(new { param1 = values1, param2 = values2,..., paramN = valuesN, })

       參數及響應結果以下表:

參  數

返  回 URL

說  明

year=2007, month=1, day=12 /reports/2007/1/12 直接匹配
year=2007, month=1 /reports/2007/1 有默認值,day=1
year=2007, month=1,
day=12, category=123
/reports/2007/1/12?category=123 溢出參數進入到 URL 的查詢字符串中
year=2007 返回空值 沒有爲匹配提供足夠的參數

揭祕路由如何綁定到操做

       這裏介紹 URL 綁定到控制器操做的底層細節,使咱們能夠更透徹的理解其中的原理。路由已經變成了一個很是通用的特性,它既不包含 MVC 的內部知識,也不依賴於 MVC。事實上,ASP.NET Web Form 和 ASP.NET Dynamic Data 都引入了路由機制。

       爲了更好的理解路由機制如何適應 ASP.NET 請求管道,下面介紹路由請求的步驟:

  1. UrlRoutingModule 嘗試使用在 RouteTable 中註冊的路由匹配當前請求。
  2. 若是有一個路由成功匹配,路由模塊就會從匹配成功的路由中獲取 IRouteHandler 接口對象。
  3. 路由模塊調用 IRouteHandler 接口的 GetHandler 方法,並返回用來處理請求的 IHttpHandler 對象。
  4. 調用 HTTP 處理程序中的 ProcessRequest 方法,而後把要處理的請求傳給它。
  5. 在 ASP.NET MVC 中,IRouteHandler 是 MvcRouteHandler 類的一個實例,MvcRouteHandler 轉而返回一個實現了 IHttpHandler 接口的 MvcHandler 對象。返回的 MvcHandler 對象主要用來實例化控制器,並調用其中的操做方法。
相關文章
相關標籤/搜索