[ASP.NET
MVC Mavericks Road]07 - URL 라우팅
ASP.NET Web Forms에서 URL 요청은 종종 aspx 페이지에 해당하고 aspx 페이지는 요청 처리가 포함된 실제 파일입니다.
ASP.NET MVC에서 URL 요청은 해당 컨트롤러의 작업에 의해 처리되며, URL 라우팅은 MVC에게 올바른 컨트롤러와 작업을 찾는 방법을 알려줍니다.
일반적으로 URL 라우팅에는 URL 구문 분석과 URL 생성이라는 두 가지 주요 기능이 포함되어 있습니다. 이 기사에서는 이 두 가지 주요 사항에 중점을 둘 것입니다.
이 기사의 디렉토리
URL 라우팅 정의 방법
다음과 같은 간단한 URL부터 시작하겠습니다.
http: //mysite.com/Admin/Index
도메인 이름 뒤에는 기본적으로 "/"가 사용되어 URL을 분할합니다. 라우팅 시스템은 {controller}/{action} 형식의 문자열을 통해 이 URL의 Admin 및 Index 조각이 각각 Controller 및 Action의 이름에 해당한다는 것을 알 수 있습니다.
기본적으로 라우팅 형식에서 "/"로 구분된 세그먼트 수는 URL 도메인 이름 뒤에 오는 세그먼트 수와 동일합니다. 예를 들어 {controller}에 대해서는 두 개의 세그먼트만 일치합니다. /{액션} 형식입니다. 다음 표와 같이
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}"); }
이 방법은 간결하고 읽기 쉽습니다.
샘플 준비
시연으로 먼저 데모를 준비합시다. 표준 MVC 애플리케이션을 만든 다음 HomeController, CustomerController 및 AdminController라는 세 가지 간단한 컨트롤러를 추가합니다. 코드는 다음과 같습니다.
HomeController
CustomerController
AdminController
/Views/Shared 폴더 아래의 세 컨트롤러에 ActionName.cshtml이라는 공유 뷰를 추가합니다. 코드는 다음과 같습니다.
ActionName.cshtml
RouteConfig.cs 파일에서 프로젝트가 자동으로 생성한 URL Routing 정의를 삭제한 후 위에서 언급한 라우팅 정의 지식을 바탕으로 가장 간단한 정의를 직접 작성했습니다:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("MyRoute", "{controller}/{action}"); }
프로그램이 실행되고 URL은 Admin/Index에 있습니다. 실행 결과를 살펴보세요.
이 데모에서 출력되는 것은 호출된 Controller 및 Action의 이름입니다. .
조각 변수에 대한 기본값 정의
위에서 특정 컨트롤러 및 작업에 대한 URL을 찾아야 합니다. 그렇지 않으면 MVC가 어떤 작업을 실행할지 모르기 때문에 프로그램에서 오류를 보고합니다. URL이 해당 조각을 제공하지 않는 경우 기본값을 지정하여 MVC에 특정 기본값을 사용하도록 지시할 수 있습니다. 컨트롤러와 액션의 기본값을 다음과 같이 지정하세요:
routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "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 来看一下我们取到的自定义片段变量的值:
... <p>The controller is: @ViewBag.Controller</p> <p>The action is: @ViewBag.Action</p> <p>The custom variable is: @ViewBag.CustomVariable</p> ...
将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 中的代码所示:
... <p>The controller is: @ViewBag.Controller</p> <p>The action is: @ViewBag.Action</p> <p> @Html.ActionLink("This is an outgoing URL", "CustomVariable") </p>
这里的Html.ActionLink方法将会生成指向View对应的Controller和第二个参数指定的Action,我们可以看看运行后页面是如何显示的:
经过查看Html源码,我们发现它生成了下面这样的一个html链接:
<a href="/Home/CustomVariable">This is an outgoing URL</a>
这样看起来,通过Html.ActionLink生成URL似乎并没有直接在View中自己写一个标签更直接明了。 但它的好处是,它会自动根据路由配置来生成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,如下所示:
<p>
@Html.ActionLink("This targets another controller", "Index", "Admin")
</p>
它会自动生成如下链接:
<a href="/Admin">This targets another controller</a>
生成带有URL参数的链接
有时候我们想在连接后面加上参数以传递数据,如 ?id=xxx 。那么我们可以给Html.ActionLink方法指定一个匿名类型的参数,如下所示:
<p> @Html.ActionLink("This is an outgoing URL", "CustomVariable", new { id = "Hello" }) </p>
它生成的Html如下:
<a href="/Home/CustomVariable/Hello">This is an outgoing URL</a>
指定链接的Html属性
通过Html.ActionLink方法生成的链接是一个a标签,我们可以在方法的参数中给标签指定Html属性,如下所示:
<p> @Html.ActionLink("This is an outgoing URL", "Index", "Home", null, new {id = "myAnchorID", @class = "myCSSClass"}) </p>
这里的class加了@符号,是因为class是C#关键字,@符号起到转义的作用。它生成 的Html代码如下:
<a class="myCSSClass" href="/" id="myAnchorID">This is an outgoing URL</a>
生成完整的标准链接
前面的都是生成相对路径的URL链接,我们也可以通过Html.ActionLink方法生成完整的标准链接,方法如下:
<p> @Html.ActionLink("This is an outgoing URL", "Index", "Home", "https", "myserver.mydomain.com", " myFragmentName", new { id = "MyId"}, new { id = "myAnchorID", @class = "myCSSClass"}) </p>
这是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 方法,使用方法如下:
<p>This is a URL: @Url.Action("Index", "Home", new { id = "MyId" }) </p>
它显示到页面是这样的:
根据指定的路由名称生成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")
它会生成如下链接:
在Action方法中生成URL<a Length="8" href="/App/Index?Length=5">Click me</a>
通常我们一般在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使用统一的风格或习惯。
以上就是[ASP.NET MVC 小牛之路]07 - URL Routing的内容,更多相关内容请关注PHP中文网(m.sbmmt.com)!