【好吧,終於要承認其實我很懶...過了許久,沒有控制器,也沒有視圖,是Routing,在很久以前ASP.NET MVC1的時候,路由是很唬人的東西,發展到現在到算是返璞歸真,技術路線也變的清晰明了,即使已經使用熟練的朋友也不妨看看,里面有很多有趣的內容。還有由於本章沒有大篇幅的代碼...所以就原諒我沒有對代碼排版吧~】
本章焦點
- 所有關於URL的事情
- Routings 101
- 窺視路由的內部原理
- 關於路由的高級用法
- 路由的可擴展性和魔力
- 怎么在Web窗體中使用路由
當涉及到源代碼的時候,就會有些對代碼風格過於痴戀的開發人員對一些例如代碼縮進風格或是大括號的位置進行激烈爭執,甚至大打出手。
所有在使用ASP.NET構建時,就會遇到如下的URL:
http://example.com/albums/list.aspx?catid=17313&genreid=33723&page=3
我們為使用所有注意力關注代碼,為什么不能付出相應多的注意力關注URL呢?可能URL並沒有那么重要,但是URL和Web用戶界面同樣使用廣泛。
本章將會幫助你建立有邏輯的控制器和URL的映射。這里面會包含ASP.NET的路由功能,ASP.NET MVC框架也會提供大量URL映射的API方法。本章首先會介紹如何使用MVC的路由,然后在進一步研究一下作為獨立功能的路由引擎。
了解URL
易用性專家Jakob Nielsen(www.useit.com)提醒開發者要注意網址,並提供了高品質的網址准則。好的網址應該遵循:
- 容易記憶,容易拼寫的域名;
- 盡量短的地址;
- 易於輸入的地址;
- 反映站點內容的結構化的網址;
- 容易理解的地址,以允許用戶通過更改地址尾部訪問到更高級別的內容;
- 持久而不經常更改的地址。
許多傳統Web框架(例如:ASP、JSP、PHP、ASP.NET)的URL都對應着磁盤上的物理文件。例如當看到網址http://example.com/albums/list.aspx可以打賭這個網站的“album”文件夾中包含有list.aspx文件。
在這種情況下,URL是直接關聯到物理磁盤文件上的。當Web服務器收到一個URL請求時,就會執行與此文件相關聯的文件代碼。
但是基於MVC的web框架大多數都不是使用網址和文件系統一對一對映關系,ASP.NET MVC也是如此。這些框架一般是是將URL映射到類調用對應的方法,而不是物理文件。
如你看到第2章中看到的,這些類通常被稱為控制器,因為它的目的就是控制用戶輸入和系統其他部分的相互作用。服務相應的方法一般稱其為動作,這些是用戶發起輸入或請求時控制器響應用戶的方法。
這時候那些習慣於通過網址訪問文件的人可能會不習慣於“統一資源定位器”這個概念。在這種情況下,資源是一個抽象的概念。當然,這只是意味這可以通過調用方法或是其他的方式獲取結果。
URI一般表示“統一資源標識符”,而URL則是指“統一資源定位器”,所以所有的URL都是技術上的URI。W3C標識說,在www.w3.org/TR/uri-clarification/中定義URL的非正式概念:使用URI代表資源標識並通過URL訪問這個資源。Ryan McDonough(www.damnhandy.com)有一個說法:URI是資源的標識,但是通過網址會給出具體信息,以獲取該資源。
這些不過是一些語義上的說法,無論說哪個大多數人都會明白你的意思。但是,這種討論有益於提醒你,在學習MVC時URL對應的是動作,而非Web服務器的硬盤某處物理位置上的靜態文件。所有說的這一切關於URL的內容都會貫穿全書。
路由介紹
ASP.NET MVC框架中的路由,有兩個主要功能:
- 匹配傳入的請求,不匹配文件系統上的文件,而是請求映射到控制器上的動作;
- 負責構造傳出的URL對應控制器上的動作。
上述兩條只是描述ASP.NET MVC應用程序中的路由。在本章的后面部分,我們將會深入挖掘和揭示由ASP.NET提供的其他路由擴展功能。
比較路由和URL重寫
為了更好的理解路由,很多開發者都在比較它與URL重寫的區別。畢竟這兩種技術方法都有助於創建傳入的URL與最終請求相分離,進而可用於提供漂亮的URL獲得更好的搜索引擎優化(SEO)。
關鍵的區別在於,URL重寫是將一個URL映射到另外一個URL。例如,URL重寫通常只是使用新的網址映射到舊的URL上面,而路由是將URL映射到資源集中。
你可以能會說,路由體現了網址以資源為中心的觀點。在這種情況下,URL代表Web上的資源(不一定是頁面)。ASP.NET路由是執行傳入請求代碼獲取資源的一種路由,這種路由負荷URL的特點,但是它不是重寫URL。
另一個比較重要的區別是,路由可以比較方便的按照URL規則生成相應的路由映射,而URL重新只是適應URL傳入請求而不能產生原始的URL。
從另一個角度來看ASP.NET路由就像一個雙向的URL重寫。這比較不足的是,ASP.NET的l.uyou從來都沒有重寫過你的網址,在請求的整個生命周期中,用戶在瀏覽器中看到的都是你的應用程序的URL。
定義路由
每個ASP.NET MVC應用程序至少會定義一個路由來約定應用程序如何處理請求,但是通常最后后悔定義很多。這樣可以想象,一個相對比較復雜的應用程序可能會有幾十個甚至更多的路由。
在本節中,你會看到如何定義路由。定義路由首先要定義URL模式,用它來指定與路由的匹配的模式。伴隨着路由的網址,也可以為路由指定默認值和URL的匹配約定,嚴格控制在何時或何種情況下如何匹配URL傳入的請求。
路由也可以通過路由的名稱將其添加到路由集合中,我們會在稍后討論路由的命名規則。
在下面的章節中,你會從一個非常簡單的例子開始創建路由。
路由地址
當ASP.NET MVC的Web應用程序項目新建完畢后,在Global.asax.cs中,你會發現Application_Start方法中包含一個RegisterRoutes方法的調用。這個就是為應用程序注冊路由的方法。
開發團隊提示 我們認為在通過定義RegisterRoutes方法來添加路由要對直接在Application_Start方法中將路由添加到RouteTable中更便於維護和進行單元測試。這樣,你可以在Global.asax.cs中通過很簡單的代碼將路由實例添加到RouteColletion中並為其編寫單元測試代碼:
var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes);
//Write tests to verify your routes here…
想知道更多關於路由的單元測試的信息,請查閱第12章“路由測試”。 |
讓我們清除現有路由代碼,並替換上一個簡單的路由。清除完畢,請定義如下路由方法:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(“simple”, “{first}/{second}/{third}”);
}
代碼片段 9-2
定義路由最簡單的形式只包含路由的名稱和URL的匹配規則,路由名稱我們會在稍后進行討論,現在我們把關注重點放在URL匹配規則上。
如表9-1所顯示的,我們在代碼9-2中定義規則在請求時路由如何根據規則將請求網址解析成鍵和值存儲在RouteValueDictionary類型的實例中。
表9-1:網址請求參數映射示例
網址 |
網址參數 |
/albumns/display/123 |
first = "ablums" second = "display" third = "123" |
/foo/bar/baz |
first = "foo" second = "bar" third = "baz" |
/a.b/c-d/-d-f |
first = "a.b" second = "c-d" third = "d-f" |
請注意,代碼9-2中的網址,有幾個URL段組成(段不包含斜杠),其中每個都包含一個參數分隔符,並使用大括號包裹起來,這些參數被稱為URL參數。
使用模式匹配規則來確定請求是否是符合這個路由定義。在這個例子中,此規則與將於任何三段的URL進行匹配,因為默認情況下,URL參數匹配任何非空值。當路由與URL的三段參數相匹配時,該URL的第一段將對應{first}參數,URL的第二部分將對應{second}參數,值的第三部分將會對應{third}參數。
在這種情況下,你可以隨意命名任何需要的參數(使用字母、數字以及允許使用的其它字符)。當收到請求時,路由會解析URL並放入路由參數字典對象中(可以在RequestContext中訪問RouteValueDictionary對象),使用URL參數的鍵對應URL段的名稱(基於位置)。
在稍后,你將會學習到在MVC應用程序的路由中,某些參數名的特殊用途。在之前表9-1展示了如何將URL轉換成RouteValueDictionary對象值。
路由值
如果你真的按照表9-1列出的地址進行訪問,會發現應用程序返回的請求結果是404文件未找到錯誤。雖然你可以定義任何你想要的參數名稱的路由,但是在ASP.NET MVC應用程序中正確的定義方式是——{Controller}和{action}。
通過{Controller}參數值實例化一個控制器類來處理請求。按照慣例,MVC會嘗試通過{Controller}參數名稱加“Controller”后綴查找實現System.Web.Mvc.Icontroller接口的類型。
再來看看那個簡單的路由實例,讓我們改回原來的樣子:
routes.MapRoute(“simple”, “{first}/{second}/{third}”);
更改為:
routes.MapRoute(“simple”, “{controller}/{action}/{id}”);
代碼段9-1
這里面已經包含了MVC特定的URL參數名稱。
現在如果我們再去看表9-1中請求路由的示例,當請求/albums/display/123時,就會有一個名為"albums"的{controller}參數。ASP.NET MVC就會要求有一個名稱加“Controller”后綴的類型“AlbumsController”。如果存在具有該名稱且實現了IController接口的類型,它將會被實例化並用來處理請求。
{action}參數表示要調用處理當前請求的控制器的方法。請注意,此調用方法只適用於從基類System.Web.Mvc.Controller集成而來的控制器類。也可以直接繼承IController接口來實現自定義請求的處理代碼的映射規則。
接着看示例/ablums/display/123,MVC將調用AlbumsController的Display方法。
請注意,表9-1中第三個URL是有效的URL,但是它將無法匹配執行a.bController控制器中名為c-d的方法,因為這並不是有效的方法的名字!除此以為其他請求只要相應的{controller}和{action}存在都可以執行。例如,假設如下控制器存在:
public class AlbumsController : Controller
{
public ActionResult Display(int id)
{
//Do something
return View();
}
}
代碼9-4
當請求/albums/display/123時,MVC將實例化這個類並調用Display方法,並傳入id參數123。
在前面路由地址的例子中{Controller}/{action}/{id},每個URL參數占據整個URL段,這並不是必須的。路由網址可以在段內加入文字值,例如,你可以為現有MVC站點添加請求時的URL前綴單詞,參加下面代碼:
site/{controller}/{action}/{id}
代碼9-5
這表明,為了符合規則要求,URL的第一部分必須是以“site”開始。因此,不能訪問/ablums/display/123,而是訪問/site/ablums/display/123。URL段允許有混合字符的參數,唯一的限制,不允許有兩個URL參數連接在一起。因此:
{language}-{country}/{controller}/{action}
{controller}.{action}.{id}
是不合法的路由地址,但是:
{controller}{action}/{id}
代碼9-6
也是不合法的路由地址。
有沒有辦法可以在請求時讓路由知道URL的組成,從那里開始是控制器,到那里結束是動作。
瀏覽如下的示例(表9-2),將會幫助你了解可以匹配哪些URL地址規則:
表 9-2 路由網址規則及示例
路由地址規則 |
地址匹配示例 |
{controller}/{action}/{genre} |
/ablums/list/rock |
Service/{aciton} -{format} |
/service/display-xml |
{report}/{year}/{month}/{day} |
/sales/2008/1/23 |
路由的默認值
到目前為止,本章已經完整的介紹為路由定義一個完整的URL匹配規則的方法。在請求時並不是只有原來的一種匹配規則,也可以為路由的URL參數添加默認值,例如,假設有一個沒有參數的動作:
public class AlbumsController : Controller
{
public ActionResult List()
{
//Do something
return View();
}
}
當然你可能想通過URL調用這個方法:
/ablums/list
然而,由於在前面的代碼段中定義過URL的規則,{controller}/{action}/{id},這將是無法正常工作的,因為這個請求地址必須包含規則定義的三個參數數,而不是只有兩個。
在這點上,似乎你需要為這個路由規則重新定義上一個代碼片段,改為只包含兩個部分:{controller}/{action}。如果可以不重新定義另外一個路由而是將第三個參數設置為可選,豈不是更好?
幸運的是,你可以!路由的API為參數提供了精細的參數默認值設置。例如,你可以這樣定義路由:
routes.MapRoute(“simple”, “{controller}/{action}/{id}”,
new {id = UrlParameter.Optional});
代碼 9-9
代碼段{id = UrlParamter.Optional}定義了參數{id}的默認值。這個設置可以讓路由在匹配請求時可以忽略id參數。換句話說,這個路由使用這個三段網址規則可以匹配任意兩段或三段的URL。
提示: 需要注意,URL也可以通過設置id為空字符串{id=""}。這樣似乎更簡潔,為什么不使用這個呢?有什么區別嗎? |
現在就可以允許請求URL “/albums/list”,這已經達成了我們的目標,接下來讓我們看看還可以設置什么樣的默認值。
在下面的代碼片段中,演示了為路由的{action}和其他多個參數添加默認值:
routes.MapRoute(“simple”
, “{controller}/{action}/{id}”
, new {id = UrlParameter.Optional, action=”index”});
代碼 9-10
開發團隊提示: |
這個例子中為URL的{action}提供了一個存入Route類的字典屬性中的默認值。通常情況下,URL需要對{controller}/{action}這兩個部分進行匹配。但是現在為第二個參數添加了默認值,路由就可以只匹配{Controller}參數而忽略{action}參數了。在這種情況下,{action}參數是由默認值給出的而不是傳入URL。
現在對應之前表中的URL匹配規則在開看一下表9-3的內容:
表9-3: URL匹配規則
路由URL規則 |
默認值 |
網址匹配示例 |
{controller}/{action}/{id} |
new {id = URLParameter.Optional} |
/albumns/display/123 /albumns/display |
{controller}/{action}/{id} |
new{controller = "home", action="index",id = URLParameter.Optional} |
/albumns/display/123 |
在這里你要明白,URL參數的位置要比默認值更重要。例如,給定的URL規則{controller}/{action}/{id},不為{action}和{id}指定默認值,而是通過定義多個路由也可以實現,只是這樣的話,每個路由的功能性都會變弱。可能你會問,這是為什么呢?
看一個簡單的例子,你就能明白。假設你定義了兩個路由,第一個包含{action}參數的默認值:
routes.MapRoute(“simple”, “{controller}/{action}/{id}”, new {action=”index “});
routes.MapRoute(“simple2”, “{controller}/{action}”);
現在如果發起請求/albumns/rock,應該匹配哪個規則呢?因為已經為{action}提供了默認值而{id}為“rock”,所以應該匹配第一個?還是應該匹配第二個規則,將{action}設置為“rock”?
在這個例子中,到底哪個路由更符合請求似乎很模糊。為了避免含糊不清,路由引擎規定,當我們為{action}設置默認值后,也應該為它后面的{id}提供一個默認值。
當URL段內有文本值時,路由會采用不同的方式的進行解析。假設定義如下路由:
routes.MapRoute(“simple”, “{controller}-{action}”, new {action = “index”});
代碼 9-11
請注意,URL參數{controller}和{action}之間有字符“-”進行分割。如果使用/albumns-list請求時很明確就是使用這個規則進行匹配,但是如果請求/ablumns-呢?應該不會,因為會忽略這個URL。
事實證明,當路由在解析請求URL,並且未在URL段內匹配到任何文本參數值。在這種情況下,默認值開始發揮作用,生成完整的URL。請參見本章后面的小節“引擎內部:路由如何生成URL”。
路由規則
有的時候,你可能更多的是需要控制URL而不是指定URL的段數。例如,下面這兩個網址:
這些網址的格式是本章目前提高的默認路由所能匹配的三段式網址。如果你不小心請求了系統將會搜索名為“2008Controller”的控制器並調用名為“01”的方法。然后你可以告訴它們這個網址映射的是不同的東西。我們怎么才能做到這一點?這些規則是非常有用的,它允許你使用正則表達式與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}”);
在第一個路由中包含三個URL參數{year},{month}和{day}。初始化匿名對象{year=@”\d{4}”, month=@”\d{2}”, day=@”\d{2}”}用來聲明參數映射字典中的規則。如你所見,{year}參數的限制條件是正則表達式“\d{4}”,表示這個參數只匹配4位數字。
這個字符串的正則表達式格式和.NET Framework中的Regex類使用的是完全相同的(事實上,路由引擎就是使用的Regex類)。如果請求參數與當前規則不匹配的將會自動轉入下一個路由進行判斷。
如果你熟悉正則表達式,你就會知道其實正則表達式“\d{4}”可以匹配任何包含四個連續整數的字符串,例如“abc1234def”。
路由會自動為規則字符串添加“^”和“&”字符,以確保該值能被完整匹配。換句話說,在當前這種情況下,使用的是表達式“^\d{4}&”,而不是“\d{4}”,以確保能完全匹配“1234”而不是“abcd1234”。
完成定義之后,“/2008/05/25”將會與代碼段9-1中的第一條路由相匹配,但是不會匹配“\08\05\25”,因為“08”並無法滿足“\d{4}”的條件。
提示:請注意,我們把新的路由放在默認路由之前。注意路由順序,因為我們必須先對“\2008\06\07”進行匹配。 |
默認情況下,可以使用正則表達式字符串來執行請求URL匹配,但是如果你仔細看就會發現規則字典類型RouteValueDictionary是實現接口IDictionary<string,object>。這意味着該字典類型對象的值是對象而不是string類型。這將為傳入請求的參數值提供了非常大的靈活性。如果利用這一優勢,請參閱“自定義路由規則”這一節。
命名路由
在ASP.NET路由工作的大多數情況下是不會要求使用路由的名稱的。創建一個路由只需要給定匹配規則然后調整好路由順序,其他的將給路由引擎來完成就可以了。但是在本章中,你會看到在有些情況下會按照不同的路由規則來生成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“/code/p/index/123”,后面的會生成"/home/index/123",都生成了復合我們期望的鏈接。
在通常情況下,這是非常精簡的,但是有些情況下也會產生煩惱。
假設你需要在路由列表的頂部添加路由規則“/static/url”來處理靜態頁“/aspx/somepage.aspx”請求:
routes.MapPageRoute(“new”, “static/url”, “~/aspx/SomePage.aspx”);
請注意,你能將這個路由排在路由列表的RegisterRoutes方法的后面,因為如果那樣它永遠都不能匹配傳入的請求。為什么不能呢?當傳入“/static/url”時會優先匹配默認路由。因此你需要將這個路由添加到默認路由列表的頂端。
提示:注意這個問題並不是專門針對Web窗體的路由,在很多情況下,你可能會用到很多非ASP.NET MVC路由來處理問題。 |
將這條路由定義在路由列表的開始看起來是否能足夠好的處理變化,這樣做正確嗎?當傳入請求時,這個路由只會匹配到“/static/url”這唯一URL,這正是我們想要的,但是如果生成URL呢?現在回頭去看看剛才調用URL.RouteLink方法生成的鏈接,你就會發現它們出問題了:
/url?controller=section&action=Index&id=123
和
/static/url?controller=Home&action=Index&id=123
咦?!
這將會是一個難以辨識的路由請求,在人們不時的請求中,很難確定會使用哪個實例進行處理。
一般來說,就我們在本章前面討論過的,生成URL要根據你提供的值來填充URL參數。
當你有一個新路由{controller}/{action}/{id}時,生成新URL需要提供控制器、動作和id的值。在這種情況下,當出現一個沒有任何URL參數的新路由,從技術上說,每次生成URL都會與它進行匹配,“路由參數會為每一個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}
)
就像保加利亞著名小說家Elias Canetti說的“人民的名字就是他們的命運”,使用路由生成URL的是一樣的。
MVC Areas
Areas是在ASP.NET MVC 2中推出的,讓你可以使用模型、視圖或控制器進行獨立功能划分。這也意味着你可以對更大、更復雜的網站進行逐個划分,使他們更易於管理。
Area 路由注冊
要配置Area路由就需要繼承並創建AreaRegistration類的子類並重寫AreaName和RegisterArea成員。ASP.NET MVC的默認項目模版中,在Global.asax的Application_Start方法中,調用了AreaRegistration.RegisterAllAreas方法。你會在第13章看到一個完整的例子,路由是如何調用AreaRegistration.RegisterAllAreas進行工作的。
Area 路由沖突
如果在應用程序根部的同一區域內有兩個相同名稱的控制器,並在請求時沒有提供相應的名字空間進行匹配就會獲得一個比較詳細的錯誤消息:
發現多個控制器控制器類型匹配名稱“Home”。這可能是應為路由服務器在請求(‘{controller}/{action}/{id}’)時,沒有指定名字空間,以幫助搜索相匹配的控制器。如果在這樣的情況下,需要重寫“MapRoute”注冊方法,以重載獲得“namespaces”參數。
請求“Home”發現以下匹配控制器:
AreasDemoWeb.Controllers.HomeController
AreasDemoWeb.Areas.MyArea.Controllers.HomeController
當使用添加Area的對話框添加“Area”后,路由將會在注冊這個Area並生成相應的命名空間,這將會確保路由在該區域只匹配唯一的控制器。
當路由匹配時可以使用命名空間來縮小控制器的范圍。當路由有一個命名空間參數時,只能匹配該命名空間內的控制器對象。但是如果沒有指定命名空間的情況下,所有控制器對於路由都是有效的。
如果沒有指定命名空間而出現兩個同名的控制器,就會產生模糊不清的異常。
另一種防止異常長生的方式就是在項目中使用特殊的控制器名稱。然而,你可能有更多的理由需要使用同名的控制器(例如,不想影響生成路由的網址)。在這種情況下,你就需要為控制器指定特定的命名空間。列表9-1顯示你如何做到這一點:
列表9-1
routes.MapRoute(
“Default”,
“{controller}/{action}/{id}”,
new { controller = “Home”, action = “Index”, id = “” },
new [] { “AreasDemoWeb.Controllers” }
);
在前面的代碼中,還提供了第四個參數一個數組對象包含要解析的命名空間。示例代碼的控制器在命名空間“AreasDemoWeb.Controllers”中。
捕獲參數
捕獲參數可以使路由匹配任意多個URL參數段。它可以把沒有定義的參數段部分當作查詢字符串。
例如,通過列表9-2所定義的路由處理表9-4中所展示的請求。
列表9-2
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(“catchallroute”, “query/{query-name}/{*extrastuff}”);
}
表9-4:針對列表9-2的請求
URL |
參數值 |
/query/select/a/b/c |
extrastuff="a/b/c" |
/query/select/a/b/c/ |
extrastuff="a/b/c" |
/query/select/select/ |
extrastuff="" |
多URL參數段
如前所述,路由的URL每個段都可以包含多個參數。例如,下面就是的有效路由規則:
- {title}-{artist}
- Album{title}and{artist}
- {filename}.{ext}
為了避免混淆,參數是不可以連續的。例如,下面這些錯誤的規則:
- {title}{artist}
- Download{filename}{ext}
在請求傳入時,路由會嘗試與URL值進行完全匹配。因為路由的匹配原理與正則表達式完全相同,所以它也是貪婪匹配URL參數的。另一方面,路由會試圖盡可能多的匹配文本,以滿足每個URL參數。
例如,使路由{fiulename}.{ext}匹配請求/asp.net.mvc.xml是怎么做到的呢?如果{filename}不使用貪婪的匹配方式就只能匹配到“asp”而{ext}就會匹配到“net.mvc.xml”。但是因為URL參數使用的是貪婪方式,所有{filename}就會盡其所能匹配到“asp.net.mvc”。它不能完全匹配而必須要將剩余的“xml”留給{ext}進行匹配。
表9-5演示了如何多個路由匹配多參數的例子,請注意如果你使用{foo=bar}來標識就表示URL參數{foo}的默認值為“bar”。
表9-5:路由匹配多參數網址
路由網址 |
請求網址 |
路由返回 |
{filename}.{ext} |
/Foo.xml.aspx |
filename="Foo.xml" ext="aspx" |
My{title}-{cat} |
/MyHouse-dwelling |
location="House" cat="dwelling" |
{foo}xyz{bar} |
/xyzxyzxyzblah |
foo="xyzxyz" |
請注意第一個例子,在匹配URL“/foo.xml.apx”時,{filename}並沒有在匹配到第一個“.”就只捕獲結果字符串“foo”而停止匹配。而是貪婪匹配到“Foo.xml”。
StopRoutingHandler和IgnoreRoute
默認情況下,路由會忽略處理針對磁盤映射的物理文件的訪問。這就是為什么,如CSS、JS或JPG的文件可以用正常的方式訪問的到。
但是有些時候,即使不是針對磁盤上文件的請求,你也不希望路由來處理。例如,使用ASP.NET 的WebResource.axd來請求Web資源,這是通過HttpHandler來完成的而不是直接訪問磁盤文件。
使用StopRoutingHandler的方式可以確保路由忽略這樣的請求。清單9-3展示如果手工添加一個路由,然后創建一個新的StopRoutingHandler並添加到RouteCollection中。
清單9-3
public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
“{resource}.axd/{*pathInfo}”,
new StopRoutingHandler()
));
routes.Add(new Route
(
“reports/{year}/{month}”
, new SomeRouteHandler()
));
}
如果請求“/WebResource.axd”程序會自動匹配第一個路由對象,而第一個路由又會返回一個StopRouteingHandler,路由系統會將其轉入正常的ASP.NET環境交給默認HTTP程序來處理.axd擴展的映射。
還有一種更簡單的方式就是告訴路由忽略這個請求,這個被恰當的命名為“IgnoreRoute”。這個就是RouteCollection類型的一個擴展方法就像你以前見過的MapRoute方法一樣。使用這種新方法非常方便,在清單9-4中實現清單9-3:
清單9-4
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(“{resource}.axd/{*pathInfo}”);
routes.MapRoute(“report-route”, “reports/{year}/{month}”);
}
是不是更簡潔更易懂了?接下來你會在ASP.NET MVC中發現更多像MapRoute和IgnoreRoute一樣簡潔的擴展方法。
調試路由
過去很長一段時間,Visual Studio都無法調試路由,因為路由是ASP.NET內部處理的邏輯,根本無法定義斷點。經常會有一些控制器在請求路由時出現錯誤而中斷你的應用程序。為了展示路由的錯誤,或缺確定列表中生效路由的位置,你就需要在代碼中加入更多的內容,也會讓事情看起來更加混亂,會話調試變的更加沮喪。
當路由的調試被啟動后,你可以使用DebugRouteHandler來取代你所有的路由。這個路由會捕獲並處理所有傳入的請求,並會在頁面地步顯示所有路由表、路由參數以及診斷數據。
要使用RouteDebugger只需要在NuGet中輸入命令——install-Package RouteDebugger。添加RouteDebugger包后,需要在web.config中的appSettings中添加打開路由調試的設置:
清單9-5
<add key=”RouteDebugger:Enabled” value=”true” />
只要是路由調試啟用,就會顯示路由對當前請求在地址欄中格式的要求(見圖例9-1)。這樣你可以看到地址欄中不同的網址的匹配情況。在下面部分顯示了應用程序中定義的所有路由。你可以看到匹配當前網址的路由。
圖例9-1
提示:我提供了完整的路由調試的源代碼,所以你可以自己定義輸出所需要數據的代碼。例如,Stephen Walther在RoutingDebugger的基礎上創建了路由調試控制器。因為它是與控制器級別掛鈎的,所以它只能處理匹配的路由請求,單從純粹的調試方面這使得它沒有那么強大,但是它確實為沒有禁用的路由提供了很多便利的好處。雖然現在還在爭議是否應該對路由進行單元測試,但是你可以使用這個調試控制器在已知的路由上執行自動化測試。Stephen的控制器地址可以訪問他的博客獲得:http://tinyurl.com/RouteDebuggerController |
核心:路由如何生成URL
到目前為止,本章主要集中討論路由如何匹配傳入的請求URL,這也是路由的主要功能,另外一個路由系統重要功能就是構造URL。當生成URL時,首先要選擇匹配請求URL的路由來生成URL。這構成了完成的路由傳入和傳出的雙向處理系統。
開發團隊提示:讓我們花點時間來理解一下這兩句話。“生成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)
第一個方法接受RequestContext和用戶指定的路由值(字典)兩個參數值來選擇所需的路由。
- 通過調用Route.GetVirtualPath方法,路由將會遍歷每個路由來詢問:“你能生成這些參數的URL嗎?”。這與路由匹配傳入請求時是使用相同的匹配邏輯。
- 如果有路由相應(也就是說它可以匹配),將會返回包含一個VirtualPathData的實例、相關的URL信息和匹配信息。如果不是,將會返回null,路由系統將會轉入下一個路由。
第二個重載方法接受第三個參數——路由名稱,路由名稱是路由集合中的主鍵標識,沒有兩個路由可以使用相同的名稱。當指定路由名稱后,引擎將不會再針對路由集合進行循環進行參數匹配。相反,它會直接進入指定路由並進行接下來的步驟。加入這個路由與傳入參數並不匹配,這個方法將會直接返回NULL而不會再與其他路由進行匹配。
詳解URL生成
Route類提供了很多具體實施的高級算法。
示例 |
下面是大多數開發人員都會使用到的路由以及相關的詳細邏輯和步驟。
圖例 9-2 圖例9-3 |
Ambient Route Values
在某些生成URL的時候,並沒有為GetVirtualPath方法提供明確的參數。讓我們來看看下面的這個例子:
示例: |
||||||||
假設你需要顯示一個非常大的任務清單,但是並不想一次就全部顯示給用戶,而是需要他們通過鏈接來分頁加載。如圖9-4展示了一個非常簡單的任務列表分頁界面。
圖例9-4
通過點擊“previous”和“next”按鈕將頁面數據導航到上一頁或者下一頁,但是這些所有請求都是有同一個控制器和動作來完成的。
下面這個路由就來處理這個請求:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute(“tasks”, “{controller}/{action}/{page}”, new {controller=”tasks”, action=”list”, page=0 }); } 代碼段9-13
為了生成上一頁和下一頁的鏈接,我們需要為路由指定所有所需的URL參數。所以需要生成到第二頁的鏈接,我們需要在視圖里面使用下面的代碼:
@Html.ActionLink(“Page 2”, “List”, new {controller=”tasks”, action=”List”, page = 2})
但是,我們可以利用路由的環境將其設置縮短,下面是任務頁面的URL。
/tasks/list/2
這個請求的路由數據如下(表9-6):
表9-6:路由數據
要生成下一頁的URL,我們需要在新的請求中更改指定的路由數據。
@Html.ActionLink(“Page 2”, “List”, new { page 2}) 代碼段9-14
即使ActionLink請求只提供了頁面參數,路由引擎會根據環境值在路由列表中進行控制器和動作查找匹配,環境值是請求的RouteData中的當前值,明確所提供的控制器和動作值后,當然就會重寫環境值。 |
參數溢出
參數溢出是指在URL生成時候提供了路由沒有定義的值。根據定義路由的網址是默認的字典型和約束性字典。需要的注意的是,溢出參數是從未使用過的環境值。
在請求路由時生成URL時,溢出參數會成為生成的URL后追加的查詢字符串。
再次,來看一個非常有啟發性的案例。假設有如下的默認路由:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
“Default”,
“{controller}/{action}/{id}”,
new { controller = “Home”, action = “Index”, id = UrlParameter.Optional }
);
}
現在假設你要生成這樣一個URL,在使用路由生成時,你額外傳遞了一個路由值“page=2”。請注意,路由定義沒有包含“page”這個參數。在這個例子中,沒有生成URL,而是使用Url.RouteUrl方法。
@Url.RouteUrl(new {controller=”Report”, action=”List”, page=”123”})
代碼段9-16
URL將會生成為“/Report/List?page=2”。如你所見,當我們指定參數與默認路由是不匹配的,事實上,我們指定了比定義要多的參數。在這種情況下,這些額外的參數被追加為查詢字符串參數。最重要的事情是,路由沒辦法找到哪個路由有足夠的項完全匹配。換句話說,只要指定參數滿足路由的定義,額外的參數並不重要。
其他URL生成示例
讓我們來定義如下路由:
void Application_Start(object sender, EventArgs e)
{
routes.MapRoute(“report”,
“reports/{year}/{month}/{day}”,
new {day = 1}
);
}
代碼段9-17
下面使用Url.RouteUrl來生成一些URL:
@Url.RoutUrl(new {param1 = value1, parm2 = value2, ..., parmN, valueN})
代碼段9-18
參數和URL結果如表9-7所示。
表9-7:參數和GetVirtualPath的URL結果
參數 |
生成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 |
返回null。 |
沒有匹配到足夠的參數。 |
核心:路由是怎么將URL匹配到動作的
本節內容主要是深入引擎核心,了解路由和MVC是如何划分以及如何聯系在一起的。
通過人們會將路由功能誤解為ASP.NET MVC的一個功能子集。在ASP.NET MVC 1.0預覽版的時候是這樣的,但是很快路由作為一項非常實際的功能已經獨立於ASP.NET MVC框架。例如,ASP.NET動態數據團隊,也會使用到路由功能。從這點上講,路由變成了一個更為通用的功能模塊,而不是內部知識或會依賴於MVC。
為了能更好的了解到路由處理APS.NET管道中的請求,讓我們來看一下路由請求所涉及到的步驟。
這里主要討論路由在集成模式的IIS7.0(及以上)。IIS7.0經典模式或者IIS6在使用的時候可能會有細微的差別。當使用Visual Studio的內置Web服務器時,該行為默認為IIS7集成模式。 |
深入理解:路由的請求管道
路由管道包含以下幾個層次:
- 當路由規則注冊到RouteTable之后,UrlRoutingModule會試圖匹配當前請求;
- 如果路由匹配成功,從路由模塊匹配相應的IRouteHandler;
- 路由模塊會調用IRouteHandler的GetHandler方法並返回用來處理請求的IHttpHandler對象;
- HTTP處理程序將會調用ProcessRequest將請求交給相應的處理程序來處理;
- 在ASP.NET MVC 中,IRouteHandler是MvcRouteHandler,相應的MvcHandler也是繼承自IHttpHandler,MvcHandler負責實例化控制器,並調用控制器中相應的動作方法。
RouteData
回想一下,調用GetRouteData方法會返回一個RouteData實例。RouteData到底是什么呢?RouteData中包含符合匹配要求的請求信息。
在前面部分我們定義了這個URL規則:{controller}/{action}/{id}。當接收到一個請求“/ablums/list/123”,路由就會嘗試匹配這個請求,如果匹配成功,就會創建一個字典型對象,其中包含從URL中解析出來的信息。具體的說,它會從URL段中逐個取出URL參數值存入字典中。
在實例{controller}/{action}/{id}中,字典中至少包含三個鍵:“controller”、“action”和“id”。在請求“/albums/list/123”中,路由會解析相應URL的字典值,controller=albums,action=list和id=123。
自定義路由約束
在本章前面的“路由限制”一節中介紹了如果使用正則表達式的細粒度來控制路由匹配。你可能還記得,我們提到的字符串字典對象RouteValueDictionary類。當你傳遞一個字符串作為約束時,Route類會使用正則表達式解析這個字符串。路由其實可以使用正則表達式以外的限制。
路由提供了IRouteConstraint接口實現一個Match方法,下面你可以看到這個接口的定義:
public interface IRouteConstraint
{
bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection);
}
代碼段9-19
當路由獲取路由約束時,路由引擎會查找並調用實現了IRouteConstraint的約束對象。引擎會調用路由約束的Match方法會確定給定請求是否滿足約束的要求。
路由本身提供了一種實現這種接口的類HttpMethodConstraint類。這個約束允許你指定路由並只能滿足一種HTTP請求方式(verbs)。
例如,如果你想要路由響應GET請求,但是不響應 POST、PUT或DELETE請求,有可以通過以下方式定義:
routes.MapRoute(“name”, “{controller}”, null
, new {httpMethod = new HttpMethodConstraint(“GET”)} );
代碼段9-20
請注意,自定義約束,並沒有提供對應的URL參數。因此,它可以對其他的如請求表頭或基於多個URL參數做出約束。 |
在Web表單中使用路由
雖然這本書的焦點是ASP.NET MVC,但是路由也是ASP.NET的核心功能,所以你也需要了解在Web Forms中如何很好的使用它。本節着眼於通常情況下在ASP.NET 4的WebForms中完全嵌入路由功能。
在ASP.NET 4中,你需要為Global.asax文件添加System.Web.Routing的應用聲明,然后定義一個與ASP.NET MVC應用程序中格式相同的Web Forms路由聲明:
void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
private void RegisterRoutes(RouteCollection routes)
{
routes.MapPageRoute(
“product-search”,
“albums/search/{term}”,
“~/AlbumSearch.aspx”);
}
代碼段9-21
唯一一個真正與MVC路由不同的是最后一個參數,這里是直接路由到真正的Web Forms頁面。當然,你也可以使用Page.RouteData訪問路由參數,像這樣:
protected void Page_Load(object sender, EventArgs e)
{
string term = RouteData.Values[“term”] as string;
Label1.Text = “Search Results for: “ + Server.HtmlEncode(term);
ListView1.DataSource = GetSearchResults(term);
ListView1.DataBind();
}
代碼段9-22
你也可以在頁面中使用<asp:RouteParameter>很方便的將對象值綁定到一個數據庫查詢命令中。例如,在前面使用過的路由“/albumns/search/beck”,你可以通過下面的SQL命令來傳遞查詢值:
<asp:SqlDataSource id=”SqlDataSource1” runat=”server”
ConnectionString=”<%$ ConnectionStrings:Northwind %>”
SelectCommand=”SELECT * FROM Albums WHERE Name LIKE @searchterm + ‘%’”>
<SelectParameters>
<asp:RouteParameter name=”searchterm” RouteKey=”term” />
</SelectParameters>
</asp:SqlDataSource>
你也可以使用RouteValueExpressionBuilder,寫出比Page.RouteValue["key"]更優雅的代碼。如果你想在Label中搜索term,你可以這樣做:
<asp:Label ID=”Label1” runat=”server” Text=”<%$RouteValue:Term%>” />
代碼段9-24
同樣你也可以使用隱藏代碼的Page.GetRouteUrl()方法來生成URL:
string url = Page.GetRouteUrl(
“product-search”,
new { term = “chai” });
代碼段9-25
使用相應路由的RouteUrlExpressionBuilder也可以構造輸出URL:
<asp:HyperLink ID=”HyperLink1”
runat=”server”
NavigateUrl=”<%$RouteUrl:SearchTerm=Chai%>”>
Search for Chai
</asp:HyperLink>
代碼段9-26
小結
路由很像中國的圍棋游戲:很簡單,但是學習和掌握可能需要一輩子。哦,好吧不是一輩子,但是肯定也需要至少幾天的時間。在本章介紹的都是些路由的基本概念,而在現實的ASP.NET MVC(或者是WebForms)應用中會非常復雜。