路由 - ASP.NET MVC 4 系列

       软件开发人员常常对一些细小的细节问题倍加关注,由其在考虑源代码的质量和结构时更是如此。因此,当遇到大部分使用 ASP.NET 技术构建的站点,使用如下的 URL 地址时,可能会有些奇怪:

http://example.com/albums/list.aspx?catid=17173&genreid=33723&page=3

       既然我们对代码倍加重视,为什么不能同样的重视 URL 呢?虽然它看上去并不是那么重要,但它却是一种合法且广泛使用的 Web 用户接口!

理解 URL(Uniform Resource Locator)

       可用性专家力劝开发人员重视 URL,并指出高质量的 URL 应该满足以下几点要求:便于记忆和拼写、简短、便于输入、可以反映出站点结构、“可破解的”,用户可以移除 URL 的末尾,进而到达更高层次的信息体系结构、持久,不能改变

       按照传统,在很多 Web 框架中(如 ASP、JSP、PHP、ASP.NET 等),URL 代表的是磁盘上的物理文件,例如上面的 URL 我们可以确定站点的目录结构中有一个 albums 文件夹,且还包含一个 List.aspx 文件。URL 和文件系统的这种对应关系,并不适用于大部分基于 MVC 的 Web 框架,这类框架应用不同的方法把 URL 映射到某个类的方法调用,而不是磁盘上的某个物理文件

       URL 是统一资源定位符的首字母所写,资源是一种抽象概念,既可以指一个文件,也可以指方法调用的结果或服务器上的一些其他内容

       URI 代表统一资源标识符,从技术角度看,所有 URL 都是 URI。W3C 认为 URL是一个非正式的概念,它通过表示自身的主要访问机制来标识资源。而有专家提出另一种看法:URI 是某资源的标识符,URL 则为获取该资源提供了具体的信息

路由概述

       ASP.NET MVC 框架中的路由主要有两种用途:

  1. 匹配传入的请求,并把这些请求映射到控制器操作。
  2. 构造传出的 URL,用来响应控制器中的操作。

       很多开发人员喜欢把路由与 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:

http://example.com/2008/01/23

http://example.com/posts/categories/aspnetmvc

       它们都包含 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&amp;action=Index&amp;id=123">Test</a>
<a href="/static/url?controller=Home&amp;action=Index&amp;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:

  • {title}-{artist}
  • Album{title}and{artist}
  • {filename}.{ext}

       为了避免二义性,我们规定参数不能临近,下面列出的 URL 都是无效的:

  • {title}{artist}
  • Download{filename}{ext}

       路由 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 的路由相匹配,这样路由就可以在处理传入传出 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 返回空值,并且不会再匹配其他路由。

URL 生成详解

       Route 类提供了前面高层次算法的具体实现:

  1. 开发人员调用像 Html.ActionLink 或 Url.Action 之类的方法,这些方法反过来再调用 RouteCollection.GetVirtualPath 方法,并向它传递一个 RequestContext 对象、一个包含值的字典、选择生成 URL 的路由名称(可选参数)。
  2. 路由机制查看要求的路由 URL 参数(即没有提供 URL 参数的默认值),并确保提供的路由值字典为每一个要求的参数提供一个值。否则,URL 生成程序会立即停止,并返回空值。
  3. 一些路由可能包含没有对应 URL 参数的默认值。例如,路由可能为 category 键提供一个默认值“pastries”,但是 category 不是路由 URL 的一个参数。这种情况下,如果用户传入的路由值字典为 category 提供了一个值,那么该值必须匹配 category 的默认值!
  4. 路由系统应用路由的约束,如果有的话。
  5. 路由匹配成功。现在可以查看每一个 URL 参数,并尝试用字典中的对应值填充对应参数,进而生成 URL。

       image

       image

溢出参数(overflow parameters)

       指在 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 请求管道,下面介绍路由请求的步骤:

  1. UrlRoutingModule 尝试使用在 RouteTable 中注册的路由匹配当前请求。
  2. 如果有一个路由成功匹配,路由模块就会从匹配成功的路由中获取 IRouteHandler 接口对象。
  3. 路由模块调用 IRouteHandler 接口的 GetHandler 方法,并返回用来处理请求的 IHttpHandler 对象。
  4. 调用 HTTP 处理程序中的 ProcessRequest 方法,然后把要处理的请求传给它。
  5. 在 ASP.NET MVC 中,IRouteHandler 是 MvcRouteHandler 类的一个实例,MvcRouteHandler 转而返回一个实现了 IHttpHandler 接口的 MvcHandler 对象。返回的 MvcHandler 对象主要用来实例化控制器,并调用其中的操作方法。

posted on 2016-07-08 11:42  SkySoot  阅读(428)  评论(0编辑  收藏  举报

导航