我們知道在ASP.NET Web Forms中,一個URL請求往往對應一個aspx頁面,一個aspx頁面就是一個物理檔案,它包含對請求的處理。
而在ASP.NET MVC中,一個URL請求是由對應的一個Controller中的Action來處理的,由URL Routing來告訴MVC如何定位到正確的Controller和Action。
籠統的講,URL Routing包含兩個主要功能:解析URL 和 生成URL,本文將圍繞這兩個大點進行講解。
本文目錄
URL Routing 的定義方式
讓我們從下面這樣一個簡單的URL開始:
http://mysite.com/Admin/Index
在域名的後面,預設使用“/”來對URL進行分段。路由系統通過類似於 {controller}/{action} 格式的字串可以知道這個URL的 Admin 和 Index 兩個片段分別對應Controller和Action的名稱。
預設情況下,路由格式中用“/”分隔的段數是和URL域名的後面的段數是一致的,比如,對於{controller}/{action} 格式只會匹配兩個片段。如下表所示:
URL路由是在MVC工程中的App_Start資料夾下的RouteConfig.cs檔案中的RegisterRoutes方法中定義的,下面是建立一個空MVC專案時系統生成的一個簡單URL路由定義:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
靜態方法RegisterRoutes是在Global.asax.cs檔案中的Application_Start方法中被呼叫的,除了URL路由的定義外,還包含其他的一些MVC核心特性的定義:
protected void Application_Start() { AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
RouteConfig.RegisterRoutes方法中傳遞的是 RouteTable 類的靜態 Routes 屬性,返回一個RouteCollection的例項。其實,“原始”的定義路由的方法可以這樣寫:
public static void RegisterRoutes(RouteCollection routes) { Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute", myRoute); }
建立Route物件時用了一個URL格式字串和一個MvcRouteHandler物件作為建構函式的引數。不同的ASP.NET技術有不同的RouteHandler,MVC用的是MvcRouteHandler。
這種寫法有點繁瑣,一種更簡單的定義方法是:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
這種方法簡潔易讀,一般我們都會用這種方法定義路由。
示例準備
作為演示,我們先來準備一個Demo。建立一個標準的MVC應用程式,然後新增三個簡單的Controller,分別是HomeController、CustomerController和AdminController,程式碼如下:
public class HomeController : Controller { public ActionResult Index() { ViewBag.Controller = "Home"; ViewBag.Action = "Index"; return View("ActionName"); } }
public class CustomerController : Controller { public ActionResult Index() { ViewBag.Controller = "Customer"; ViewBag.Action = "Index"; return View("ActionName"); } public ActionResult List() { ViewBag.Controller = "Customer"; ViewBag.Action = "List"; return View("ActionName"); } }
public class AdminController : Controller { public ActionResult Index() { ViewBag.Controller = "Admin"; ViewBag.Action = "Index"; return View("ActionName"); } }
在 /Views/Shared 資料夾下再給這三個Controller新增一個共享的名為 ActionName.cshtml 的 View,程式碼如下:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>ActionName</title> </head> <body> <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> </body> </html>
我們把RouteConfig.cs檔案中專案自動生成的URL Rounting的定義刪了,然後根據前面講的路由定義知識,我們自己寫一個最簡單的:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
程式執行,URL定位到 Admin/Index 看看執行結果:
這個Demo輸出的是被呼叫的Controller和Action名稱。
給片段變數定義預設值
在上面我們必須把URL定位到特定Controller和Action,否則程式會報錯,因為MVC不知道去執行哪個Action。 我們可以通過指定預設值來告訴MVC當URL沒有給出對應的片段時使用某個預設的值。如下給controller和action指定預設值:
routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" });
這時候如果在URL中不提供action片段的值或不提供controller和action兩個片段的值,MVC將使用路由定義中提供的預設值:
它的各種匹配情況如下表所示:
注意,對於上面的URL路由的定義,我們可以只給action一個片段指定預設值,但是不能只給controller一個片段指定預設值,即如果我們給Controller指定了預設值,就一定也要給action指定預設值,否則URL只有一個片段時,這個片段匹配給了controller,action將找不到匹配。
定義靜態片段
並不是所有的片段都是用來作為匹配變數的,比如,我們想要URL加上一個名為Public的固定字首,那麼我們可以這樣定義:
routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });
這樣,請求的URL也需要一個Public字首與之匹配。我們也可以把靜態的字串放在大括號以外的任何位置,如:
routes.MapRoute("", "X{controller}/{action}", new { controller = "Home", action = "Index" });
在一些情況下這種定義非常有用。比如當你的網站某個連結已經被使用者普遍記住了,但這一塊功能已經有了一個新的版本,但呼叫的是不同名稱的controller,那麼你把原來的controller名稱作為現在controller的別名。這樣,使用者依然使用他們記住的URL,而導向的卻是新的controller。如下使用Shop作為Home的一個別名:
routes.MapRoute("ShopSchema", "Shop/{action}", new { controller = "Home" });
這樣,使用者使用原來的URL可以訪問新的controller:
自定義片段變數
自定義片段變數的定義和取值
contrlloer和action片段變數對MVC來說有著特殊的意義,在定義一個路由時,我們必須有這樣一個概念:contrlloer和action的變數值要麼能從URL中匹配得到,要麼由預設值提供,總之一個URL請求經過路由系統交給MVC處理時必須保證contrlloer和action兩個變數的值都有。當然,除了這兩個重要的片段變數,我們也可從通過自定義片段變數來從URL中得到我們想要的其它資訊。如下自定義了一個名為Id的片段變數,而且給它定義了預設值:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" });
我們在HomeController中增加一個名為CustomVariable的ACtion來演示一下如何取自定義的片段變數:
public ActionResult CustomVariable() { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = RouteData.Values["id"]; return View("ActionName"); }
可以通過 RouteData.Values[segment] 來取得任意一個片段的變數值。
再稍稍改一下ActionName.cshtml 來看一下我們取到的自定義片段變數的值:
... <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> <div>The custom variable is: @ViewBag.CustomVariable</div> ...
將URL定位到 /Home/CustomVariable/Hello 將得到如下結果:
自定義的片段變數用處很大,也很靈活,下面介紹一些常見的用法。
將自定義片段變數作為Action方法的引數
我們可以將自定義的片段變數當作引數傳遞給Action方法,如下所示:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View("ActionName"); }
效果和上面是一樣的,只不過這樣省去了用 RouteData.Values[segment] 的方式取自定義片段變數的麻煩。這個操作背後是由模型繫結來做的,模型繫結的知識我將在後續博文中進行講解。
指定自定義片段變數為可選
指定自定片段變數為可選,即在URL中可以不用指定片段的值。如下面的定義將Id定義為可選:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
定義為可選以後,需要對URL中沒有Id這個片段值的情況進行處理,如下:
public ActionResult CustomVariable(string id) { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id == null ? "<no value>" : id; return View("ActionName"); }
當Id是整型的時候,引數的型別需要改成可空的整型(即int? id)。
為了省去判斷引數是否為空,我們也可以把Action方法的id引數也定義為可選,當沒有提供Id引數時,Id使用預設值,如下所示:
public ActionResult CustomVariable(string id = "DefaultId") { ViewBag.Controller = "Home"; ViewBag.Action = "CustomVariable"; ViewBag.CustomVariable = id; return View("ActionName"); }
這樣其實就是和使用下面這樣的方式定義路由是一樣的:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" });
定義可變數量的自定義片段變數
我們可以通過 catchall 片段變數加 * 號字首來定義匹配任意數量片段的路由。如下所示:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
這個路由定義的匹配情況如下所示:
使用*catchall,將匹配的任意數量的片段,但我們需要自己通過“/”分隔catchall變數的值來取得獨立的片段值。
路由約束
正規表示式約束
通過正規表示式,我們可以制定限制URL的路由規則,下面的路由定義限制了controller片段的變數值必須以 H 打頭:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*" } );
定義路由約束是在MapRoute方法的第四個引數。和定義預設值一樣,也是用匿名型別。
我們可以用正規表示式約束來定義只有指定的幾個特定的片段值才能進行匹配,如下所示:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", action = "^Index$|^About$" } );
這個定義,限制了action片段值只能是Index或About,不區分大小寫。
Http請求方式約束
我們還可以限制路由只有當以某個特定的Http請求方式才能匹配。如下限制了只能是Get請求才能進行匹配:
routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new { controller = "^H.*", httpMethod = new HttpMethodConstraint("GET") } );
通過建立一個 HttpMethodConstraint 類的例項來定義一個Http請求方式約束,建構函式傳遞是允許匹配的Http方法名。這裡的httpMethod屬性名不是規定的,只是為了區分。
這種約束也可以通過HttpGet或HttpPost過濾器來實現,後續博文再講到濾器的內容。
自定義路由約束
如果標準的路由約束滿足不了你的需求,那麼可以通過實現 IRouteConstraint 介面來定義自己的路由約束規則。
我們來做一個限制瀏覽器版本訪問的路由約束。在MVC工程中新增一個資料夾,取名Infrastructure,然後新增一個 UserAgentConstraint 類檔案,程式碼如下:
public class UserAgentConstraint : IRouteConstraint { private string requiredUserAgent; public UserAgentConstraint(string agentParam) { requiredUserAgent = agentParam; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return httpContext.Request.UserAgent != null && httpContext.Request.UserAgent.Contains(requiredUserAgent); } }
這裡實現IRouteConstraint的Match方法,返回的bool值告訴路由系統請求是否滿足自定義的約束規則。我們的UserAgentConstraint類的建構函式接收一個瀏覽器名稱的關鍵字作為引數,如果使用者的瀏覽器包含註冊的關鍵字才可以訪問。接一來,我們需要註冊自定的路由約束:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("ChromeRoute", "{*catchall}", new { controller = "Home", action = "Index" }, new { customConstraint = new UserAgentConstraint("Chrome") } ); }
下面分別是IE10和Chrome瀏覽器請求的結果:
定義請求磁碟檔案路由
並不是所有的URL都是請求controller和action的。有時我們還需要請求一些資原始檔,如圖片、html檔案和JS庫等。
我們先來看看能不能直接請求一個靜態Html檔案。在專案的Content資料夾下,新增一個html檔案,內容隨意。然後把URL定位到該檔案,如下圖:
我們看到,是可以直接訪問一靜態資原始檔的。
預設情況下,路由系統先檢查URL是不是請求靜態檔案的,如果是,伺服器直接返回檔案內容並結束對URL的路由解析。我們可以通過設定 RouteCollection的 RouteExistingFiles 屬性值為true 讓路由系統對靜態檔案也進行路由匹配,如下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
設定了routes.RouteExistingFiles = true後,還需要對IIS進行設定,這裡我們以IIS Express為例,右鍵IIS Express小圖示,選擇“顯示所有應用程式”,彈出如下視窗:
點選並開啟配置檔案,Control+F找到UrlRoutingModule-4.0,將這個節點的preCondition屬性改為空,如下所示:
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/>
然後我們執行程式,再把URL定位到之前的靜態檔案:
這樣,路由系統通過定義的路由去匹配RUL,如果路由中沒有定義該靜態檔案的匹配,則會報上面的錯誤。
一旦定義了routes.RouteExistingFiles = true,我們就要為靜態檔案定義路由,如下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
這個路由匹配Content/StaticContent.html的URL請求為controller = Customer, action = List。我們來看看執行結果:
這樣做的目的是為了可以在Controller的Action中控制對靜態資源的請求,並且可以阻止對一些特殊資原始檔的訪問。
設定了RouteExistingFiles屬性為true後,我們要為允許使用者請求的資原始檔進行路由定義,如果每種資原始檔都去定義相應的路由,就會顯得很繁瑣。
我們可以通過RouteCollection類的IgnoreRoute方法繞過路由定義,使得某些特定的靜態檔案可以由伺服器直接返回給給瀏覽器,如下所示:
public static void RegisterRoutes(RouteCollection routes) { routes.RouteExistingFiles = true; routes.IgnoreRoute("Content/{filename}.html"); routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
這樣,只要是請求Content目錄下的任何html檔案都能被直接返回。這裡的IgnoreRoute方法將建立一個RouteCollection的例項,這個例項的Route Handler 為 StopRoutingHandler,而不是 MvcRouteHandler。執行程式定位到Content/StaticContent.html,我們又看到了之前的靜態面面了。
生成URL(連結)
前面講的都是解析URL的部分,現在我們來看看如何通過路由系統在View中生成URL。
生成指向當前controller的action連結
在View中生成URL的最簡單方法就是呼叫Html.ActionLink方法,如下面在 Views/Shared/ActionName.cshtml 中的程式碼所示:
... <div>The controller is: @ViewBag.Controller</div> <div>The action is: @ViewBag.Action</div> <div> @Html.ActionLink("This is an outgoing URL", "CustomVariable") </div> ...
這裡的Html.ActionLink方法將會生成指向View對應的Controller和第二個引數指定的Action,我們可以看看執行後頁面是如何顯示的:
經過檢視Html原始碼,我們發現它生成了下面這樣的一個html連結:
<a href="/Home/CustomVariable">This is an outgoing URL</a>
這樣看起來,通過Html.ActionLink生成URL似乎並沒有直接在View中自己寫一個<a>標籤更直接明瞭。 但它的好處是,它會自動根據路由配置來生成URL,比如我們要生成一個指向HomeContrller中的CustomVariable Action的連線,通過Html.ActionLink方法,只需要給出對應的Controller和Action名稱就行,我們不需要關心實際的URL是如何組織的。舉個例子,我們定義了下面的路由:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("NewRoute", "App/Do{action}", new { controller = "Home" }); routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
執行程式,我們發現它會自動生成下面這樣的連線:
<a href="/App/DoCustomVariable">This is an outgoing URL</a>
所以我們要生成指向某個Action的連結時,最好使用Html.ActionLink方法,否則你很難保證你手寫的連線就能定位到你想要的Action。
生成其他controller的action連結
上面我們給Html.ActionLink方法傳遞的第二個引數只告訴了路由系統要定位到當前View對應的Controller下的Action。Html.ActionLink方法可以使用第三個引數來指定其他的Controller,如下所示:
<div> @Html.ActionLink("This targets another controller", "Index", "Admin") </div>
它會自動生成如下連結:
<a href="/Admin">This targets another controller</a>
生成帶有URL引數的連結
有時候我們想在連線後面加上引數以傳遞資料,如 ?id=xxx 。那麼我們可以給Html.ActionLink方法指定一個匿名型別的引數,如下所示:
<div> @Html.ActionLink("This is an outgoing URL", "CustomVariable", new { id = "Hello" }) </div>
它生成的Html如下:
<a href="/Home/CustomVariable/Hello">This is an outgoing URL</a>
指定連結的Html屬性
通過Html.ActionLink方法生成的連結是一個a標籤,我們可以在方法的引數中給標籤指定Html屬性,如下所示:
<div> @Html.ActionLink("This is an outgoing URL", "Index", "Home", null, new {id = "myAnchorID", @class = "myCSSClass"}) </div>
這裡的class加了@符號,是因為class是C#關鍵字,@符號起到轉義的作用。它生成 的Html程式碼如下:
<a class="myCSSClass" href="/" id="myAnchorID">This is an outgoing URL</a>
生成完整的標準連結
前面的都是生成相對路徑的URL連結,我們也可以通過Html.ActionLink方法生成完整的標準連結,方法如下:
<div> @Html.ActionLink("This is an outgoing URL", "Index", "Home", "https", "myserver.mydomain.com", " myFragmentName", new { id = "MyId"}, new { id = "myAnchorID", @class = "myCSSClass"}) </div>
這是Html.ActionLink方法中最多引數的過載方法,它允許我們提供請求的協議(https)和目標伺服器地址(myserver.mydomain.com)等。它生成的連結如下:
<a class="myCSSClass" id="myAnchorID" href="https://myserver.mydomain.com/Home/Index/MyId#myFragmentName" > This is an outgoing URL</a>
生成URL字串
用Html.ActionLink方法生成一個html連結是非常有用而常見的,如果要生成URL字串(而不是一個Html連結),我們可以用 Url.Action 方法,使用方法如下:
<div>This is a URL: @Url.Action("Index", "Home", new { id = "MyId" }) </div>
它顯示到頁面是這樣的:
根據指定的路由名稱生成URL
我們可以根據某個特定的路由來生成我們想要的URL,為了更好說明這一點,下面給出兩個URL的定義:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }); }
對於這樣的兩個路由,對於類似下面這樣的寫法:
@Html.ActionLink("Click me", "Index", "Customer")
始終會生成這樣的連結:
<a href="/Customer/Index">Click me</a>
也就是說,永遠無法使用第二個路由來生成App字首的連結。這時候我們需要通過另一個方法Html.RouteLink來生成URL了,方法如下:
@Html.RouteLink("Click me", "MyOtherRoute","Index", "Customer")
它會生成如下連結:
<a Length="8" href="/App/Index?Length=5">Click me</a>
這個連結指向的是HomeController下的Index Action。但需要注意,通過這種方式來生成URL是不推薦的,因為它不能讓我們從直觀上看到它生成的URL指向的controller和action。所以,非到萬不得已的情況才會這樣用。
在Action方法中生成URL
通常我們一般在View中才會去生成URL,但也有時候我們需要在Action中生成URL,方法如下:
public ViewResult MyActionMethod() { string myActionUrl = Url.Action("Index", new { id = "MyID" }); string myRouteUrl = Url.RouteUrl(new { controller = "Home", action = "Index" }); //... do something with URLs... return View(); }
其中 myActionUrl 和 myRouteUrl 將會被分別賦值 /Home/Index/MyID 和 / 。
更多時候我們會在Action方法中將客戶端瀏覽器重定向到別的URL,這時候我們使用RedirectToAction方法,如下:
public RedirectToRouteResultMyActionMethod() { return RedirectToAction("Index"); }
RedirectToAction的返回結果是一個RedirectToRouteResult型別,它使MVC觸發一個重定向行為,並呼叫指定的Action方法。RedirectToAction也有一些過載方法,可以傳入controller等資訊。也可以使用RedirectToRoute方法,該方法傳入的是object匿名型別,易讀性強,如:
public RedirectToRouteResult MyActionMethod() { return RedirectToRoute(new { controller = "Home", action = "Index", id = "MyID" }); }
URL方案最佳實踐
下面是一些使用URL的建議:
- 最好能直觀的看出URL的意義,不要用應用程式的具體資訊來定義URL。比如使用 /Articles/Report 比使用 /Website_v2/CachedContentServer/FromCache/Report 好。
- 使用內容標題比使用ID好。比如使用 /Articles/AnnualReport 比使用 /Articles/2392 好。如果一定要使用使用ID(比如有時候可能需要區分相同的標題),那麼就兩者都用,如 /Articles/2392/AnnualReport ,它看起來很長,但對使用者更友好,而且更利於SEO。
- 對於Web頁面不要使用副檔名(如 .aspx 或 .mvc)。但對於特殊的檔案使用副檔名(如 .jpg、.pdf 和 .zip等)。
- 儘可能使用層級關係的URL,如 /Products/Menswear/Shirts/Red,這樣使用者就能猜到父級URL。
- 不區分大小寫,這樣方便使用者輸入。
- 正確使用Get和Post。Get一般用來從伺服器獲取只讀的資訊,當需要操作更改狀態時使用Post。
- 儘可能避免使用標記符號、程式碼、字元序列等。如果你想要用標記進行分隔,就使用中劃線(如 /my-great-article),下劃線是不友好的,另外空格和+號都會被URL編碼。
- 不要輕易改變URL,尤其對於網際網路網站。如果一定要改,那也要儘可能長的時間保留原來的URL。
- 儘量讓URL使用統一的風格或習慣。
參考:
《Pro ASP.NET MVC 4 4th Edition》
http://msdn.microsoft.com/en-us/library/cc668201.ASPX