[翻译]《ASP.NET MVC 3 高级编程》第九章:路由(Professional ASP.NET MVC 3 --- Chapter 9: Routing)
【好吧,终于要承认其实我很懒...过了许久,没有控制器,也没有视图,是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)应用中会非常复杂。