软件开发人员经常对一些细小的细节问题倍加关注,由其在考虑源代码的质量和结构时更是如此。所以,当遇到大部分使用 ASP.NET 技术构建的站点,使用以下的 URL 地址时,可能会有些奇怪:正则表达式
http://example.com/albums/list.aspx?catid=17173&genreid=33723&page=3算法
既然咱们对代码倍加剧视,为何不能一样的重视 URL 呢?虽然它看上去并非那么重要,但它倒是一种合法且普遍使用的 Web 用户接口!服务器
可用性专家力劝开发人员重视 URL,并指出高质量的 URL 应该知足如下几点要求:便于记忆和拼写、简短、便于输入、能够反映出站点结构、“可破解的”,用户能够移除 URL 的末尾,进而到达更高层次的信息体系结构、持久,不能改变。mvc
按照传统,在不少 Web 框架中(如 ASP、JSP、PHP、ASP.NET 等),URL 表明的是磁盘上的物理文件,例如上面的 URL 咱们能够肯定站点的目录结构中有一个 albums 文件夹,且还包含一个 List.aspx 文件。URL 和文件系统的这种对应关系,并不适用于大部分基于 MVC 的 Web 框架,这类框架应用不一样的方法把 URL 映射到某个类的方法调用,而不是磁盘上的某个物理文件。app
URL 是统一资源定位符的首字母所写,资源是一种抽象概念,既能够指一个文件,也能够指方法调用的结果或服务器上的一些其余内容。框架
URI 表明统一资源标识符,从技术角度看,全部 URL 都是 URI。W3C 认为 URL是一个非正式的概念,它经过表示自身的主要访问机制来标识资源。而有专家提出另外一种见解:URI 是某资源的标识符,URL 则为获取该资源提供了具体的信息。asp.net
ASP.NET MVC 框架中的路由主要有两种用途:post
不少开发人员喜欢把路由与 URL 重写进行对比。由于这两种方法均可用于分离传入 URL 和结束处理请求。此外,它们均可觉得搜索引擎优化(Search Engine Optimization,SEO)构建“漂亮的”URL。然而,它们也有很大的区别:URL 重写关注的是将一个 URL 映射到另外一个 URL,例如常把旧的 URL 映射到新的 URL,与之相比,路由关注的则是如何将 URL 映射到资源。性能
每一个 ASP.NET MVC 程序都至少须要一个路由来定义本身处理请求的方式,但一般,老是会有一个或多个路由,很是复杂的程序可能会有数十个甚至更多。优化
路由的定义是从 URL 模式开始的,由于它指定了与路由相匹配的模式。路由能够指定它的 URL 及其默认值,能够约束 URL 各个部分,提供关于路由如何、什么时候与传入的请求 URL 相匹配的严格控制。
如今清除 RegisterRoutes 方法中全部的代码,而后添加一个很是简单的路由,添加后以下:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("simple", "{first}/{second}/{third}");
}
MapRoute 方法的最简单形式是采用路由名称和路由的 URL 模式。下表展现了在上面代码中定义的路由如何把指定的 URL 解析成一个存储在 RouteValueDictionary 实例中的键/值对,从而能够帮助理解,路由如何把 URL 分解成稍后在请求管道中使用的重要信息片断:
URL |
URL 参数值 |
/albums/display/123 | first="albums" second="display" third="123" |
/foo/bar/baz | first="foo" second="bar" third="baz" |
/a.b/c-d/e-f | first="a.b" second="c-d" third="e-f" |
路由 URL 是由若干个 URL 段(斜杠之间全部内容)组成,每一个段都包括一组花括号限定的占位符,这些占位符就是 URL 参数。这是一种模式匹配规则,用来决定路由是否适用于传入的请求。针对本示例,因为 URL 参数在默认的状况下将匹配任何非空值,所以,示例中定义的规则能够匹配任何带有 3 个断的 URL。
当客户端的请求到达服务器时,路由解析请求的 URL,并将解析出的 路由参数值 放入字典(经过 RequestContext 访问的 RouteValueDictionary)中,在生成的字典中把路由 URL 参数名称做为 key,将对应位置上的字段做为 value。
若是真的请求上面注册的 URL,会返回 404 错误。尽管可使用任何想要的名称来定义路由,但 ASP.NET MVC 框架要求使用一些特定的参数名称:{controller}、{action}。
{controller} 参数的值用于实例化一个控制器类,按照约定,ASP.NET MVC 把 Controller 后缀添加到 {controller} URL 参数值的后面构成一个类型名称,而后根据该名称查找实现了 System.Web.Mvc.IController 接口的类型,不区分大小写。
{action} 参数值用来指明该类中须要调用的方法。
如今,咱们将路由注册代码修改成 ASP.NET MVC 约定的模式:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("simple", "{controller}/{action}/{id}");
}
参考上表第一个示例,如今变为请求名称为“album”的 controller,框架把 Controller 做为后缀添加到 URL 参数值“album”以后,从而获得类型名称“albumController”,不区分大小写,且该类型若是还实现了 IController 接口,那么该类就会被实例化,并用于处理这个请求。
注意:上表中第三个 URL 是一个有效的路由 URL,但它并不能匹配任何的控制器和操做,缘由很简单,二者都不是有效的 ASP.NET 类名和方法名。
除了 {controller} 和 {action} 以外,若是还有其余任何路由参数,它们均可以做为参数传递到操做方法中!
假设存在以下的控制器:
public class AlbumsController : Controller
{
public ActionResult Display(int id)
{
// do something...
return View();
}
}
如今若是发出请求:/albums/display/123,上述代码则彻底能被匹配。
{controller}/{action}/{id} 中每个段都包含一个 URL 参数,同时 URL 参数也占有对应的整个段。事实上,并不必定老是这样,路由 URL 在段中也容许包含字面值,若是要把 MVC 集成到一个现有的站点中,而且想让全部 MVC 请求都以 site 开头,那能够以下实现:
site/{controller}/{action}/{id} // 这个路由只有第一个段以 site 开头,才能与请求匹配。
还有更灵活的路由语法规则,在 URL 段中容许字面值和参数混合在一块儿,仅有的限制是不容许两个连续的 URL 参数:
{language}-{country}/{controller}/{action} // 合法
{controller}.{action}.{id} // 合法
{controller}{action}/{id} // 错误的,路由没法知道传入请求 URL 的控制器部分什么时候结束,操做方法部分什么时候开始
URL 模式及其匹配示例:
路由 URL 模式 |
匹配的 URL 示例 |
{controller}/{action}/{genre} | /albums/list/rock |
service/{action}-{format} | /service/display-xml |
{report}/{year}/{month}/{day} | /sales/2010/06/19 |
路由 URL 并非在匹配请求时所要考虑的惟一因素,还应该考虑为路由 URL 参数提供的默认值。
假设如今有一个没有任何参数的操做方法:
public ActionResult List()
{
// do something...
return View();
}
咱们会很天然的想到经过这样的 URL 调用 List 方法:/albums/list,然而,根据先前定义的路由 URL 就不能正常运行,由于先前的路由定义只匹配包含 3 个段的 URL,但 /albums/list 只包含 2 个段。彷佛须要从新定义一个相似这样的两个段的路由:{controller}/{action}。但若是能指出先前的路由定义中,第三个段是可选的,不是更好?
路由 API 容许为参数段提供默认值,例如:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("simple", "{controller}/{action}/{id}",
new { id = UrlParameter.Optional });
}
{ id = UrlParameter.Optional } 为 {id} 参数定义了默认值,该默认状况就容许路由匹配没有 id 参数的请求。换言之,该路由如今能够匹配具备两个段的 URL,也能够匹配具备三个段的 URL!
还能够将 id 设置为空串{id=""} 来实现上述功能,但为何不呢?先前说过,框架会解析 URL 参数值,并将解析后的内容放入一个字典中,当使用 UrlParameter.Optional 时,在 URL 中并无提供值,路由就不会在字典中添加条目,若使用空串,则路由会在字典中添加 key 为 id,值为 空 的条目。某些场合中,这种差异是重要的,可让咱们知道 id 值没有被指定和指定为空的区别。
能够为多个参数提供默认值,下面的代码为 {action} 参数提供了一个默认值:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("simple", "{controller}/{action}/{id}",
new { id = UrlParameter.Optional, action = "index" });
}
有时,相对于 URL 段的数量来讲,还须要对 URL 有更多的控制,以下两个 URL:
它们都包含 3 个段,且均可以和先前定义的默认路由相匹配。若是不当心,就会使系统查找一个名为 2008Controller 的控制器和名为 01 的方法,这显然是很荒唐的。然而,仅经过查看这些 URL,咱们如何才能知道它们应该映射到哪些内容呢?
约束容许 URL 段使用正则表达式来限制路由是否匹配请求,例如:
public static void RegisterRoutes(RouteCollection routes)
{
// 映射指定的 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}",
new { id = UrlParameter.Optional, action = "index" });
}
在路由的底层使用 Regex 类,熟悉正则表达式的语法规则,能够知道 \d{4} 实际上匹配包含有 4 个连续数字的任意字符串,如“abc1234def”,然而,路由机制会总动使用“^”和“$”符号包装指定的约束表达式,以确保表达式可以精确的匹配参数值。换言之,在这里并不能匹配 “abc1234def”。
这个路由添加在默认的 simple 路由以前,是由于路由会按前后顺序与传入的 URL 进行匹配,直到匹配成功。而 /2008/06/07 这类请求与两个定义的路由都匹配,天然要把更具体的路由放在前面。
ASP.NET 中的路由机制不要求路由具备名称,且大多数状况下没有名称的路由也能知足大多数应用场合。一般为了生成一个 URL,只需抓取预约义的路由值,并把它们交给路由引擎,剩余工做就由路由引擎来作。但有些状况下,这种方法在选择生成 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。正如指望的那样,生成了对应的 URL:
<a href="/code/p/Index/123">Test</a>
<a href="/Home/Index/123">Default</a>
假设咱们在路由列表的开始部分添加了以下的路由,以便 /aspx/SomePage.aspx 页面可以处理 URL/static/url:
routes.MapPageRoute("new", "static/url", "~/aspx/SomePage.aspx");
将上面的路由移动到定义路由列表的开始位置,看起来是无足轻重的变化,但真的是这样吗?对于传入的请求,该路由只能匹配 /static/url 的请求,这正是咱们想要的。可是如何生成 URL 呢?回到前面查看两次调用 RouteLink 返回的结果,将会发现返回的两个 URL 都是不可用的:
<a href="/static/url?controller=section&action=Index&id=123">Test</a>
<a href="/static/url?controller=Home&action=Index&id=123">Default</a>
一般,当使用路由生成 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 }
)
<a href="/code/p/Index/123">route: Test</a>
<a href="/Home/Index/123">route: Default</a>
正如先前所述,路由 URL 的每一个段均可能含有多个参数,下面这些是有效 URL:
为了不二义性,咱们规定参数不能临近,下面列出的 URL 都是无效的:
路由 URL 在与传入的请求匹配时,它的字面值是与请求精确匹配的,而其中的 URL 参数则是贪婪匹配!这与正则表达式有一样的含义,换言之,路由使每一个 URL 参数都尽量多的匹配文本。
例如,路由 {filename}.{ext} 是如何匹配 /asp.net.mvc.xml 请求的呢?若是 {filename} 不是贪婪匹配,那么它只须要匹配 asp,而由 {ext} 参数匹配剩余的 .net.mvc.xml,但因为 URL 参数要求贪婪匹配,因此 {filename} 参数会尽量匹配它能匹配的文本 asp.net.mvc,但它不能再匹配更多的了,由于必须为 .{ext} 部分留下匹配空间。
路由两大主要职责,除了以前所叙述的如何匹配传入的请求 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);
1. 路由集合经过 Route.GetVirtualPath 方法遍历每一个路由并询问:能够生成给定参数的 URL 吗?这个过程相似于路由在与传入请求匹配时所运用的逻辑。
2. 若是一个路由能够应答,那么它就返回一个包含了 URL 的 VirtualPathData 实例以及其余匹配信息,不然它就返回空值,路由机制移向列表的下一个路由。
重载版本二接收 3 个参数,多了路由名称。在路由集合中路由名称是惟一的,路由机制能够当即找到指定名称的路由,并进行上述逻辑,若指定的路由不能匹配指定的参数,Route.GetVirtualPath 返回空值,而且不会再匹配其余路由。
Route 类提供了前面高层次算法的具体实现:
指在 URL 生成过程当中使用但没有在路由定义中指定的路由值,且溢出参数会做为查询字符串参数附加在生成的 URL 以后。
例以下面第一的默认路由:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = "" }
);
若是使用这条指令渲染一个 URL:
@Url.RouteUrl(new { controller = "Report", action = "List", page = "123" })
上述代码生成的 URL 是:/Report/List?page=123
假设定义了下面的路由:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("report", "reports/{year}/{month}/{day}", new { day = 1 });
}
还有一些按照下面的通常格式,调用 Url.RouteUrl 方法后返回的结果:
@Url.RouteUrl(new { param1 = values1, param2 = values2,..., paramN = valuesN, })
参数及响应结果以下表:
参 数 |
返 回 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 | 返回空值 | 没有为匹配提供足够的参数 |
这里介绍 URL 绑定到控制器操做的底层细节,使咱们能够更透彻的理解其中的原理。路由已经变成了一个很是通用的特性,它既不包含 MVC 的内部知识,也不依赖于 MVC。事实上,ASP.NET Web Form 和 ASP.NET Dynamic Data 都引入了路由机制。
为了更好的理解路由机制如何适应 ASP.NET 请求管道,下面介绍路由请求的步骤: