軟件開發人員經常對一些細小的細節問題倍加關注,由其在考慮源代碼的質量和結構時更是如此。所以,當遇到大部分使用 ASP.NET 技術構建的站點,使用以下的 URL 地址時,可能會有些奇怪:正則表達式
http://example.com/albums/list.aspx?catid=17173&genreid=33723&page=3算法
既然咱們對代碼倍加劇視,爲何不能一樣的重視 URL 呢?雖然它看上去並非那麼重要,但它倒是一種合法且普遍使用的 Web 用戶接口!服務器
可用性專家力勸開發人員重視 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
不少開發人員喜歡把路由與 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:
它們都包含 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&action=Index&id=123">Test</a>
<a href="/static/url?controller=Home&action=Index&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 都是無效的:
路由 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 時成爲一個完整的雙向系統!
路由核心是一個很是簡單的算法,該算法基於一個由 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 返回空值,而且不會再匹配其餘路由。
Route 類提供了前面高層次算法的具體實現:
指在 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 請求管道,下面介紹路由請求的步驟: