至此為止,我們一直在使用ASP.NET MVC新項目隨帶的默認路由配置。現在我們將深入探討路由系統,並學習如何創建應用程序的自定義路由,以確保URL既是用戶友好又是搜索引擎可訪問的。
路由的全部內容都是關於URL以及如何將URL作為應用程序的外部輸入的。當使用其他開發工具,如PHP、Web Form或是經典的ASP時,URL通常對應於磁盤上的物理文件。一個http://example.com/Products.aspx這樣的URL會導致執行負責處理該請求的名為Products.aspx的文件。
通過使用URL路由,ASP.NET MVC解除了URL與物理文件的耦合。路由提供了一種把無擴展名的URL映射到控制器動作的方式,讓開發人員對URL方案有完全的控制。
在本章中,我們將介紹路由概念及其與MVC應用程序的關系,還將簡要介紹它們如何用於ASP.NET Web Form項目。我們將考察如何設計應用程序的URL方案,然后將這些概念運用於創建一個示例應用程序的路由。最后看看如何測試路由,以確保它們按預期工作。
一、介紹URL路由
與將URL綁定到磁盤上的物理文件不同,ASP.NET MVC引入的URL路由的底層結構能夠將URL映射到控制器動作,而無須在服務器上有物理文件作為URL的目標。在本節中,我們將考察新建MVC項目隨帶的默認路由的結構,以及這些路由是如何與控制器和動作的概念相關聯的。
1.默認路由
當創建一個新的ASP.NET MVC應用程序時,默認的項目模板會在Global.asax文件中調用一個名稱為RegisterRoutes的方法。該方法負責為應用程序配置路由,並定義了最初兩條路由——一條忽略路由和一條遵循{controller}/{action}/{id}模式的默認路由,如下所示。


路由是通過調用MapRoute方法而定義的,該方法有個過載。在這個例子中,默認路由是通過調用三個參數的過載來配置的。第一個參數是路由名("Default")。第二個參數是用來匹配URL的URL模式。本例中,URL模式被定義為具有三個片段——控制器、動作和ID。第三個參數是一個匿名類型,它為這些片段定義了默認值。
如果用戶訪問http://example.com/users/edit/5這樣的URL,這將匹配默認路由,因為它有三個片段,如圖所示。

在這個例子中,字符串users映射到controller參數,edit映射到actio,而5映射到id。由於這個URL顯然與路由相匹配,MVC框架會嘗試查找名稱為UsersController的類、調用Edit方法,並為其id參數傳遞5。如果找不到控制器或動作,框架會產生一個404錯誤。
添加到路由定義中的默認參數意味着URL不必精確地匹配三片段URL模式。如果指定了默認控制器Home以及默認動作Index,當控制器片段省略時,路由將默認控制器為HomeController。同樣,如果動作片段未指定,則路由會默認尋找Index動作。Id參數的默認值UrlParameter.Optional,意指不論是否指定第三個片段,路由都可以被匹配。下表給出了幾個能匹配默認路由的例子。
| URL | 路由參數 | 被選中的動作方法 |
| http://example.com/Users/Edit/5 | Controller=User, Action=Edit, id=5 | UsersController.Edit(5) |
| http://example.com/Users/Edit | Controller=User, Action=Edit | UsersController.Edit() |
| http://example.com/Users | Controller=User, Action=Index | UsersController.Index() |
| http://example.com | Controller=User, Action=Index | HomeController.Index() |
在IgnoreRoute方法中,模式{resource}.axd/{*pathInfo}確保文件擴展名為.axd的任何URL不會被路由引擎所處理,這樣才能確保任何自定義HTTP處理程序(其擴展名為.axd)以正確方式唄處理,而不會被路由引擎攔截。
2.入站與出站路由
入站路由(Inbound Routing):將URL映射到控制器或動作及任何附加參數。
出站路由(Outbound Routing):通過一組給定的路由數據(通常是控制器和動作)生成相應的URL。

上圖所示的入站路由描述了一個控制器動作的URL調用。HTTP請求進入ASP.NET管道,並通過ASP.NET MVC應用程序注冊的路由進行發送。每個路由都有處理請求的機會,而匹配路由隨后會指定被使用的控制器和動作。

二、設計URL模式(schema)
1.建立簡單、整潔的URL
傳統URL:http://example.com/eventmanagement/events_by_month.aspx?year=2011&month=4
使用路由系統的URL:http://example.com/events/2011/04
這種URL帶來的好處是,其中的日期有了一種明確的層次格式。
2.建立可破解的URL
在設計URL方案時,考慮最終用戶為了改變所顯示的數據要如何操縱或“破解”URL是有價值的。例如,也許可以合理地假設,從以下URL移去參數“04”,可能表示2011年發生的全部事件:
http://example.com/events/2011/04
同樣的邏輯可以形成下表所示的更全面的路由列表。
| URL | 描述 |
| http://example.com/events | 顯示全部事件 |
| http://example.com/events/<year> | 顯示某年事件 |
| http://example.com/events/<year>/<month> | 顯示某月事件 |
| http://example.com/events/<year>/<month>/<day> | 顯示某日事件 |
讓URL模式具有這種靈活性是很棒的,但這可能會導致應用程序中具有大量潛在的URL。在建立應用程序視圖時,你總是要改出相應的導航。記住,可能在各個頁面上不必對每個可能的URL組合都包含一個鏈接。在用戶試圖破解URL並使其生效時,讓用戶有一些驚喜的發現反而是件好事。
如果不希望用戶破解,可考慮使用連接字符來替代斜線,如/events/2008-04-01。
3.使用URL參數區分請求
讓我們對此路由加以擴展,並允許按類別列出事件。從用戶的觀點來看,最有用的URL可能像這樣:
http://example.com/events/aspnet-usergroup-meeting
但現在有問題了!我們已經有了一個與/events/<something>形式匹配的路由,用來列出特定年、月、日的事件,那么現在如何用/events/<something>也匹配類別?第二個路由片段現在意味着完全不同的含義,這與現有的路由不協調了。如果把這種URL交給路由系統,它應該把這種參數可能作類別還是日期?
幸運的是,ASP.NET MVC的路由系統允許我們運用條件,使用正則表達式來確保路由只與某個模式的參數相匹配就夠了。這意味着我們可以只用一條路由,就能讓/events/2011-01-01形式的請求傳遞給按日期顯示事件的動作,而讓/events/asp-net-mvc-in-action形式的請求傳遞給按類別顯示事件的動作。
4.盡可能避免暴露數據庫ID
一個用於托管開發人員事件的網站可能會定義這樣的URL:
http://example.com/events/87
87是從數據庫獲得的每一個對象都有一個主鍵形式的唯一標識符,但是除了數據庫管理員之外,數字87對任何人都毫無意義。因此,應該盡可能避免在URL中使用數據庫生成的ID,盡量讓它們有意義、可讀、易於理解。
http://example.com/events/houstonTechFest2010
5.考慮添加多余信息
如果必須在URL中使用數據庫ID,可考慮添加除了使URL可讀外沒什么目的的附加信息。
http://example.com/events/houstonTechFest2010/session-87
http://example.com/events/houstonTechFest2010/session-87/an-introduction-to-mvc
--------------------------------------------------------------------
搜索引擎優化(SEO)
當涉及網站的搜索引擎優化方面時,有必要提一提設計良好的URL的價值,在URL中放置一個相關的關鍵字提升搜索引擎排序。設計要點如下:
- 為控制器和動作使用描述性的、簡單的、普遍使用的單詞。力求盡可能相關並使用可能用於所建頁面的關鍵詞。
- 當在路由中包含文本參數時,用連接字符替換所有的空格符。
- 去掉字符串參數中不重要的標點和不必要的文本。
- 在URL可能的地方包含附加的、有意義的信息,如標題和描述等。
----------------------------------------------------------------------
三、在ASP.NET MVC中實現路由
默認項目模板創建了兩個默認路由,但你可以不接受這兩個默認路由的限制,添加自己的路由,以實現完全自定義的URL模式。以下將對此加以演示,以一個簡單的在線商店為例,實現幾個路由。我們將考察如何創建簡單、靜態的路由,以及創建更復雜的使用參數的路由和全匹配路由。
1.在線商店的URL模式(重要)
| 路由號 | URL | 描述 |
| 1 | http://example.com/ | 首頁,重定向到分類列表 |
| 2 | http://example.com/privacy | 顯示包含網站私有策略的靜態頁面 |
| 3 | http://example.com/products/<productcode> | 顯示相應產品代碼的產品詳情頁面 |
| 4 | http://example.com/products/<productcode>/buy | 將相應產品添加到購物籃 |
| 5 | http://example.com/basket | 顯示當前用戶的購物籃 |
| 6 | http://example.com/checkout | 啟動當前用戶的結算過程 |
注意:路由4中的URL不是設計給用戶看的,它通過表單遞交進行鏈接,在動作處理完成后會立即進行重定向,因而這種URL不會在地址欄中出現。
2.添加自定義靜態路由
路由1是由默認路由處理的。
路由2,是一個純靜態路由,將http://example.com/privacy映射到HomeController的Privacy動作。
routes.MapRoute("privacy_policcy","privacy", new { controller="Home", action="Privacy"});
警告:添加到路由表中的路由次序決定了查找匹配時的路由搜索順序。這意味着,源代碼中列出的路由,應當從帶有最具體條件的最高優先級降低到最低優先級,或全匹配路由。
3.添加自定義的動態路由
當有少量偏離一般規則的URL時,靜態路由是有用的。如果路由包含與頁面顯示的數據相關的信息,就需要動態路由了。
路由3和路由4是用兩個路由參數實現的:
routes.MapRoute("Product", "products/{productCode}/{action}", new { controller="Catalog", action="Show"});
兩個占位符將匹配URL中用斜線分隔的片段,productCode參數是必需的,但action是可選的。如果action未指定,該路由會默認指向CatalogController上的Show動作,並傳遞productCode參數。詳細代碼如下。
public class CatalogController : Controller { private ProductRepository _productRepository = new ProductRepository(); public ActionResult Show(string productCode) { var product = _productRepository.GetByCode(productCode); if (product == null) { return new NotFoundResult(); } return View(product); } }
實現一個執行時能生成HTTP 404的自定義動作結果:
public class NotFoundResult:ActionResult { public override void ExecuteResult(ControllerContext context) { context.HttpContext.Response.StatusCode = 404; new ViewResult { ViewName = "NotFound" }.ExecuteResult(context); } } }
NotFoundResult通過集成ActionResult,在其中提供了必須實現的ExecuteResult方法。該方法將響應狀態碼設置為404,然后渲染一個名為NotFound的視圖,該視圖位於Views/Shared目錄。
注意:HttpNotFoundResult動作,也將響應轉台碼設置為404,但它未提供顯示自定義錯誤頁面的機制,因此總是會給最終用戶顯現一個空屏。
最后,我們可以添加模式中的路由5和路由6。
routes.MapRoute{"catalog", "{action}",
new { controller="Catalog" },
new { action=@"basket|checkout"});
這些路由幾乎是靜態路由,只不過它們是用一個參數和一個路由約束來實現的,以保持較少的路由數目。這么做的主要原因有兩個。第一,每個請求都必須掃描路表進行匹配,所以大的路由集合會影響到性能。第二,路由越多,路由優先級問題出現的風險也越高。較少數目的路由規則更易於維護。
MapRoute方法的第四個參數包含了路由約束。約束參數是一個匿名類型形式的字典,可以用於指定如何約束特定的路由參數。在本例中,我們使用了一個正則表達式來指明,僅當片段字符串為“basket”或“checkout”時,才匹配action參數。這種約束能夠適當地阻止把未知動作傳遞給控制器。
4.全匹路由
我們現在已經添加了靜態和動態路由,以便為網站的不同URL提供內容。但假設有一個與所有路由都不匹配的請求,會發生什么?結果會拋出一個異常,這是實際應用程序中不希望發生的事情。為了對此加以處理,我們可以使用與ASP.NET的錯誤處理基礎架構結合在一起的全匹配路由。
我們將添加一個全匹配路由,用它匹配尚未被其他路由匹配的任何URL,顯示HTTP404的錯誤消息,它應該是最后一條被定義的路由。
routes.MapRoute("404-catch-all", "{*catchall}", new { controller="Error", action="NotFound"});
值catchall為全匹配路由要拾取的值提供了一個名稱。與規則路由參數不同,全匹配參數(以星號為前綴)會捕獲包括正斜線在內的整個URL部分,正斜線通常用於分隔路由參數。在上述示例中,該路由被映射到ErrorController的NotFound動作。
public class ErrorController: Controller { public ActionResult NotFound() { return new NotFoundResult(); } }
現在,可以刪去默認的{controller}/{action}/{id}路由,因為我們已經完全定制了路由,以匹配我們的URL模式。或者,你也許會選擇保留它,以作為訪問其他控制器的一種默認方式。
四、使用路由系統生成URL
每當網站中需要一個URL時,我們都要求框架給出,而不是采用硬編碼。我們需要制定一種控制器、動作以及參數的組合,剩下的由ActionLink方法完成。ActionLink是MVC框架中HtmlHelper類上的一個擴展方法,它會生成一個插入了正確URL的完整的HTML<a>元素,該URL與傳遞進來的對象參數所指定的路由相匹配。以下是調用ActionLink的一個例子:
@Html.ActionLink ( "MVC3 in Action", "Show", "Catalog", new { productCode = "mvc-in-action" }, null )
第一個是超鏈接的顯示文本;第二個和第三個指定了要被鏈接到的動作和控制器;第四個采用了一個匿名類型形式的字典,以指定任意的附加路由參數;最后一個是仍以匿名類型形式指定的任意的附加HTML屬性。
使用前面定義的路由,這個例子會生成一個鏈接,指向CatalogController上的Show動作,並帶有為productCode指定的附加參數。以下是其輸出:
<a href="/products/mvc-in-action">MVC3 in Action</a>
類似地,如果使用HtmlHelper的BeginForm方法來建立表單標簽,它會為你生成URL。有時,能夠將路由部分未指定的參數傳遞給動作時有用的:
@Html.ActionLink ( "MVC3 in Action", "Show", "Catalog", new { productCode = "mvc-in-action", currency="USD" }, null )
如果該參數與路由中的某個部分匹配,它將成為URL的一部分。否則,它將被附加到查詢字符串。比如,以下是上述代碼生成的鏈接:
<a href="/products/mvc-in-action?currency=USD">MVC3 in Action</a>
在使用ActionLink時,被選中的路由是路由集合中所定義的第一個匹配路由。大多數情況下,這是足夠的,但如果你希望請求一條特定的路由,可以使用RouteLink,它接受一個標識被請求路由的參數,像這樣:
@Html.RouteLink ( "MVC3 in Action", "Show", "Catalog", new { productCode = "mvc-in-action" }, null )
這個代碼將查找一個帶有product名稱的路由,而不是指定的控制器和動作。
有時候你需要獲得一個URL,但不是為了鏈接或表單。這通常發生在編寫Ajax代碼需要設置一個請求URL時。UrlHelper類能夠直接生成URL,由ActionLink方法和其他方法所使用。以下是一個例子:
@Url.Action ( "Show", "Catalog", new { productCode = "mvc-in-action" } )
這個代碼也返回/products/mvc-in-action,但沒有任何包圍標簽。
