一、网址路由
1.1 比对通过浏览器传来的HTTP请求
客户端对ASP.NET网站发出请求时,能通过R偶汤尼盖找到适当的HttpHandler来处理网页,大致的流程如图:
如果HttpHandler是由MvcHandler来处理,那么,此时就会进入MVC的执行生命周期,并且会找到适当的Controller与Action来对其进行处理,并将信息反馈给客户端。
1.2 将适当的网址返回浏览器
网址路由的另一个用途是决定MVC 应该输出什么样的网址并将其返回浏览器,跳转地址或在View中显示超链接时,都需要参考网址路由的定义,因为这样才能动态决定MVC输出网址应该是什么。
1.3 默认网址路由
Global.asax已经定义了两个默认的网址路由,参考图如下:
① 所有ASP.NET Web应用程序执行的入口都是HttpApplication的Application_Start()事件,所有的MVC Routing都会在此定义。其中,RoutTable.Routes是一个公开的静态对象,用于存储所有Routing的规则集(RouteCollection类)。
② 默认RegisterRoutes()方法中的IgnoreRoute()辅助方法用于定义不需要通过Routing处理的网址。
③ "{resource}"代表一个名为"resource"的Route Valueg表达式,但其实这里取任何名字都可以,它只是代表一个变量空间(PlaceHolder类),总之就是代表一个位置,用于放置一个用不到的变量。
④ "{*pathInfo}"代表一个名称为"pathInfo"的RouteValue表达式,名称前面的星号(*)的意思是"CatchAll"(抓到全部)。这个名为"pathInfo"的RouteValue表达式的值是完整的路径信息(Path Info)中扣除在③中比对到的剩余的网址。例如:网址是"/TEST.axd/a/b/c/d",则"{PathInfo}"的值为"a/b/c/d",如果没有加上星号,"{PathInfo}"的值应为"a"。其实在这里取任何名字都可以,因为它只代表一个变量的位置。
⑤ MapRoute()是最常用来定义Routing规则的辅助方法。
⑥ 定义Route的名称,在此为"Default"。
⑦ 定义网址格式和每个网址段落(PathSegment)的RouteValue表达式名称。
TIP : 该网址不能以斜线(/)开头。
⑧ 定义各RouteValue表达式的默认值,当网址路由比对不到HTTP请求网址时,就会改以默认值替代。
二、HTTP请求的URL如何对应网址路由
由于默认定义了两个网址路由,按照ASP.NET Routing的规则,当HTTP提出请求后,URL会进行网址路由的比对,而且是由上而下地一条一条比对,直到比对到符合HTTP请求的网址为止。
2.1 网址路由范例
下面举例,弄清URL和Routing之间更多概念上的联系。
[1]. http://localhost/Trace.axd/a/b/c/d/e
TIP :所有的网址都是从"http://localhost/"之后开始比对的!
- 比对顺序
(1)比对routes.IgnoreRoute命名空间的"{resource}.axd/{*pathInfo}"网址格式。
(2)"{resource}.axd"比对到"Trace.axd",因此继续比对下一个RouteValue表达式。
(3)比对"{*pathInfo}",得到"a/b/c/d/e"。
(4)因为所有的RouteValue表达式都比对成功,所以该HTTP请求会由此网址路由提供服务。
- 比对结果
该网址使用routes.IgnoreRoute命名空间进行处理,即,MVC为忽略此请求,改为ASP.NET架构本身继续进行处理。
[2] http://localhost/Trade.axd
- 比对顺序
(1)比对routes.IgnoreRoute命名空间的"{resource}.axd/{*pathInfo}"网址格式。
(2)"{resource}.axd"比对到"Trace.axd",因此继续比对下一个RouteValue表达式。
(3)比对"{*pathInfo}",由于请求的部分已经没有数据,所以理论上不会比对到任何结果。但由于"{*pathInfo}"属于CallAll,此语法会比对到包括空字符串在内的全部内容,所以这个部分也算比对成功,只是"{*pathInfo}"为空而已。
(4)因为所有的RouteValue表达式都比对成功,所以该HTTP请求会由此网址路由提供服务。
- 比对结果
该网址使用routes.IgnoreRoute命名空间进行处理,即,MVC为忽略此请求,改为ASP.NET架构本身继续进行处理。
[3] http://localhost/Member/Detail?id=123
- 比对顺序
(1)比对routes.IgnoreRoute命名空间的"{resource}.axd/{*pathInfo}"网址格式。
(2)比对请求URL的的一部分,即"Member",由于没有比对到".axd",所以比对失败。
(3)跳转到routes.MapRoute命名空间的"{controller}/{action}/{id}"网址格式。
(4)比对请求URL的第一部分,即"Member",并且比对到{controller}参数。
(5)比对请求URL的第二部分,即"Detail",并且比对到"{action}"参数。
(6)接下来的"?id=123"就不算是网址的一部分了,所以它不会被算进RouteValue表达式中,因此不会在对此比对了。
(7)"{id}"部分因为没有比对到,所以会读取默认值,也就是"UrlParameter.Optional"部分。由于存在默认值,所以也算比对成功。
(8)因为所有的RouteValue表达式都比对成功,所以该HTTP请求会由此网址路由提供服务。
- 比对结果
该网址使用routes.MapRoute命名空间进行处理,并调过MvcHandler将值赋予适当的Controller和Action程序。在这里会对应MemberController的Detail动作。
TIP :在"URL及参数"位置出现的所有路由参数都是必要的参数,必须完全符合必读规则才能比对成功;如果比对失败,就会调至下一条网址路由规则继续比对。
2.2 为网址路由加上限制条件
MapRoute()是最常用来定义Routing规则的辅助方法,它有许多应用方式(重载)。一个常用的应用方式为"样式比对规则(正则表达式)+限制条件"。
1 context.MapRoute( 2 3 "Order_default", 4 "Order/{controller}/{action}/{id}", 5 new { action = "Index", id = UrlParameter.Optional }, 6 new { id = @"\d+" } 7 );
我们在MapRoute()辅助方法中设置了4个参数,这些参数指定了一个匿名对象,其中的id属性就是我们比对{id}路由值的限制条件,其限制条件是用正则表达式(Regular Expression)来表示的。"\id+"代表比对到的{id}路由值时必须为数字才算比对成功,而这就是限制条件。
若网址为"http://localhost/Order/Member/Index/123ABC.",由于此对到的{id}路由值不符合限制条件,所以这个网址就算比对失败,接着会自动跳到下一个网址路由规则继续比对。
TIP : 这里定义的正则表达式默认是完全比对。如果你定义的样式为"\id+',事实上,在比对时会转换为"^\d+$"。
三、网址路由如何在MVC中生成网址。
URL是如何比对网址路由是网址路由的一个功能,另一个主要功能是在Controller或View中依据网址路由的定义生成适当的网址。下面介绍如何使用RouteTable.Routes.GetVirtualPath命名空间取得动态网址。
先用默认的MVC项目模块进行测试。打开网址http://localhost/Home/About,此请求所得到的路由值如图:
字段 |
值 |
controller |
Home |
action |
About |
id |
UrlParameter.Optional |
在"/Views/Home/About.aspx"页面中添加以下程序代码。
1 <%= 2 RouteTable.Routes.GetVirtualPath( 3 Request.RequestContext, 4 new RouteValueDictionary(new { page=1}) 5 ).VirtualPath 6 %>
可以看到:在RouteTable.Routes.GetVirtualPath命名空间中,第一个参数为Request.RequestContext,在会输入当前的请求信息,包括RouteValue、QueryString和其他完整的请求;第二个参数中多输入了一个RouteValueDictionary对象,并插入了一个{page}路由值。因此,在获取网址路由之前会先合并出一组路由值,如表所示:
字段 |
值 |
Action |
About |
Id |
UrlParameter.Optional |
Page |
1 |
最后,MVC会使用这组新的路由值由上而下——比对路由表(RouteTable)中所有的路由规则,已得到最适合的路由规则,并产生适当的网址。
拿以下路由规则来说,其中定义了3个路由参数,而我们得到的4个路由值中有3个路由值完全符合定义,所以,此网址路由会被选中,并且MVC会以此路由定义好的格式产生网址。
1 routes.MapRoute( 2 "Default", // 路由名称 3 "{controller}/{action}/{id}", // 带有参数的 URL 4 new 5 { // 参数默认值 6 controller = "Home", 7 action = "Index", 8 id = UrlParameter.Optional 9 } 10 );
由于{page}参数并非网址路由参数之一,因此,新建的{page}参数就被替换成了QueryString参数,输出结果如下。
/Home/About?page=1
再举一个复杂的例子。第一条路由规则定义如下:
1 routes.MapRoute( 2 "Member", // 路由名称 3 "Member/{action}/{page}", // 带有参数的 URL 4 new // 参数默认值 5 { 6 controller = "MemberCenter", 7 action = "List" 8 }, 9 new 10 { 11 action = @"Index|List|Detail", 12 page = @"\d+" 13 } 14 ); 15 routes.MapRoute( 16 "Default", // 路由名称 17 "{controller}/{action}/{id}", // 带有参数的 URL 18 new 19 { // 参数默认值 20 controller = "Home", 21 action = "Index", 22 id = UrlParameter.Optional 23 } 24 );
在"/Views/Home/About.aspx'页面中添加以下程序代码。
1 <%= 2 RouteTable.Routes.GetVirtualPath( 3 Request.RequestContext, 4 new RouteValueDictionary(new { 5 controller ="MemberCenter", 6 action="Detail" 7 }) 8 ).VirtualPath 9 10 %>
在第2个参数中将"controller"替换成"MemberCenter",将"action"替换成"Detail",所以在获得网址之前,会先合并出一组新的路由值,如表:
字段 |
值 |
controller |
MemberCenter |
action |
Detail |
id |
UrlParameter.Optional |
MVC会用这组新的路由值由上而下一一比对路由表中的所有路由规则,以得到最适合的路由规则。当此对比到第一个规则时,由于已经定义了两个路由参数,分别是"{action}"和"{page}",而我们的路由表中只有"action"没有"page",此时就会查看参数默认值中是否有默认的{page}参数,结果还是没有,因此比对失败。MVC并不会以这个网址路由来产生网址,进而跳至下一条网址路由进行比对。最后,比对的结果为"/MemberCenter/Detail"。
修改"/Views/Home/About.aspx'页面的程序代码,增加"page"参数,示例如下:
1 <%= 2 RouteTable.Routes.GetVirtualPath( 3 Request.RequestContext, 4 new RouteValueDictionary(new { 5 controller ="MemberCenter", 6 action = "Detail", 7 page = "TEST" 8 }) 9 ).VirtualPath 10 11 %>
执行以上操作后就会合并出一组新的路由值(RouteValue),如表:
字段 |
值 |
controller |
MemberCenter |
action |
Detail |
id |
UrlParameter.Optional |
page |
TEST |
当我们比对第一条规则时,由于已经定义了两个路由参数,分别是{action}和{page},而我们的路由表中已有"action"和"page"的路由值了,必要参数已经全部符合,所以会进一步比对限制条件是否符合。
由于已经定义了{page}参数的限制条件为“@"\d+"”,但路由值中的page值却是"TEST",因限制条件无法通过,所以比对还是失败了。因此,MVC并不会用这个网址路由来产生网址,进而跳至下一个网址路由进行比对。
最后比对结果为"/MemberCenter/Detail?page=TEST"。
总结:使用RouteTable.Routes.GetVirtualPath命名空间来生成网址大致会用到以下判断依据。
- 若将第1个参数带入Request.RequestContext命名空间,会预先取得当前所有的路由值。也可以输入"null"代表没有默认的路由值。
- 用当前合并后的所有路由值与网址路由表一一比对所有规则时,会先比对所有必要参数。若比对成功,就会进一步检查限制条件是否符合。
- 若找不到必要参数,就会查找参数默认值;如果仍然找不到,则比对失败。
- 若上述比对全部成功,RouteTable.Routes.GetVirtualPath命名空间就会用该网址路由定义的网址来生成网址。
RouteTable.Routes.GetVirtualPath命名空间生成网址的完整流程图。
四、MVC执行的生命周期
MVC的执行生命周期大致上分成3阶段,分别是:
(1)网址路由比对;
(2)执行Controller与Action;
(3)执行View并返回结果。
4.1 网址路由比对
当IIS收到HTTP请求后,会先通过UrlRoutingModule处理所有与网址路由有关的运算。默认下若网址可以对应到网址根目录下的实体文件,就不会通过MVC进行处理,而是直接交由IIS或ASP.NET执行。
若网址是"http://localhostContent/Site.css",由于在网站根目录下有"Content"目录,而且"Content"目录中也有Site.css文件,所有MVC不会将此网址解析成Content控制器和Site.css动作。
再举一个.NET Web Forms的例子,网址为"http://localhost/Member/Login.aspx",在Global.asax文件中新建一个特殊的网址路由,具体如下。
1 routes.MapRoute( 2 "Default_aspx", // 路由名称 3 "{controller}/{action.aspx}/{id}", // 带有参数的 URL 4 new 5 { // 参数默认值 6 controller = "Home", 7 action = "Index", 8 id = UrlParameter.Optional 9 } 10 );
在这种情况下,若在网站根目录中有使用.NET Web Forms编写的"/Member/Login.aspx"程序存在,MVC就不会应用UrlRouting,而是将流程的控制权交给IIS,并由IIS将其交友下一个模块执行。在此,就会执行"/Member/Login.aspx"程序。
如果"/Member/Login.aspx"程序不存在,那么MVC的Routing就会正式启动比对,并且在比对到上述网址后,将执行MemberController的Login动作。
若果在Global.asax文件的Application_Start()事件的最前面将RouteTableRoutes.RouteExistingFiles参数值设定为"true",MVC的UrlRouting就不会先判断是否有实体文件存在,程序代码如下:
1 protected void Application_Start() 2 { 3 RouteTable.Routes.RouteExistingFiles = true; 4 AreaRegistration.RegisterAllAreas(); 5 6 RegisterRoutes(RouteTable.Routes); 7 }
设定完后,该网站收到的所以HTTP请求都会使用RegisterRoutes()方法中定义的网址路由规则一一进行比对。若比对成功,会用MVC进行处理;否则,将执行的权利交还给IIS。
NOTE : 若在MVC中是由IgnoreRoute()辅助方法比对成功的,会导致程序直接跳离MVC执行生命周期,将程序继续执行的权利交还给IIS,由IIS觉得接下来该由哪个模块(Model)或哪个处理历程(Handler)来执行。
在使用RegisterRoutes()方法定义的网址路由规则进行比对时,事实上,比对成功后,默认值会由MvcRouteHandler决定要将改HTTP请求发送给哪个HttpHandler来执行,如图所示。MVC默认会将HTTP请求交给MvcHandler来执行。
由图所示的MvcRouteHandler程序代码可知,若要自定义RouteHandler,只要自行开发IRouteHandler接口的类,即可通过自定义的RouteHandler来决定通过网址路由比对的网址应该交给哪个HttpHandker来执行。所以,可以通过自定义RouteHandler和HttpHandler来为MVC网站提供辅助功能。
4.2 执行Controller和Action 4.3 执行View并返回结果
当程序执行到MvcHandler时,HttpHandler的入口是ProcessRequest()方法。具体这里就不分析了,见书本4.4.2和4.4.3。