MVC利用Routing实现多域名绑定一个站点、二级域名以及二级域名注册Area

     最近有这么个需求:在一个站点上绑定多个域名,每个域名进去后都要进入不同的页面。实现了这个功能以后,对于有多个域名,且有虚拟空间,但是虚拟空间却只匹配有一个站点的用户来说,可以节省很多小钱钱。

     很久以前看过《ASP.NET MVC 实现二级域名》和《ASP.NET MVC 使用二级域名来注册Area区域》这两篇文章,它们是有前后延续性的。本文的思路也是延续他们的思想来发展的,因此必须先了解前面两个文章的内容。而解决方法更是采用他们的代码加以改进实现的。在此谢谢两位作者。

1、简单实现多域名对单站点

     自己创建一个MVC的站点,大体结构如下:

程序结构

     圈1、HomeController将对应www.demo.com域名的页面;WebController将对应www.web.com的页面

     圈2、添加Test的Area将对应test.web.com的页面

     圈3、是从上面两篇文章中扒过来的域名解析的类

     然后,在global里面加上下面的语句,一个站点绑定多个域名的功能就实现了。问题肯定有,后面慢慢说。

routes.Add("demo", new DomainRoute(
                "www.demo.com",                                     
                "{controller}/{action}/{id}",
                new { controller = "Home", action = "Index", id = "" } 
            ));

routes.Add("web", new DomainRoute(
                "www.web.com",                                                    
                "{controller}/{action}/{id}",                                        
                new { controller = "Web", action = "Index", id = "" }   
            ));    
View Code

2、简单的原理说明

     首先,使用自定义路由解析的入口

     通常我们在增加自定义的路由规则时,都会用到MapRoute方法,通过"{controller}/{action}/{id}"或者"{controller}/{action}/{yyyy}/{mm}/{dd}"这样的路由规则串来自定义的路由规则。这时我们只定义了URL的path部分,并没有涉及到URL的host部分。

     仔细看看代码,MapRoute方法返回了Route类型的对象,而routes是RouteCollection类型的对象,那么我们可以这么理解:MapRoute方法生成了一个新的路由对象(Route),并把它加入到了routes这个路由集合(RouteCollection)中。其实系统还提供一个Add方法,这个方法更直接,就是往routes里添加一个RouteBase对象。而Route类型实际是继承了RouteBase的。

     到这里我们就明白了:注册一个路由新规则就是把一个新的RouteBase对象加入到路由集合中去。我们只要实现一个继承了RouteBase的类来实现对域名的解析,再把这个类型的对象加入到路由集合,就可以增加对域名的解析了。利用这个类和Add方法,我们可以扩展实现各种复杂的路由解析功能。上面例子中添加的DomainRoute类型的对象,实际就是一个解析域名的类。

     其次,为了实现二级域名以及二级域名注册Area,还要准备一些基础知识

     上面提到的RouteBase类是个抽象类,需要实现两个方法:GetRouteData和GetVirtualPath。简单的理解就是GetRouteData方法是解析url,把解析的结果按键值对存入RouteData中;而GetVirtualPath方法就是从RouteData中把url还原回来。

     RouteData类里提供了两个存放路由键值对的地方,一个Values,一个DataTokens。那么哪些数据放Values里,哪些数据放DataTokens里呢?我们拿Route类来看,参数最全的构造函数如下:

public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler);

     url:对应我们自己定义的路由规则串

     defaults:路由规则串里解析出来的键的对应缺省值

     大体上说defaults里的键值对会放到Values里,constraints和dataTokens这里不讨论,我觉得应该会放到DataTokens里,这个需要看源代码确定一下。感兴趣的同学可以自己去验证一下。实际过程中defaults会有点特殊。defaults里的“键”(键值对里的键)是和url中“{}”中的内容是一一对应的,例如:

url:"{controller}/{action}/{id}"
defaults:new { controller = "Web", action = "Index", id = "" } 

     也就是说url规定有哪些键,defaults说明这些键对应的值是什么。其中,controller和action可以认为是类似保留字这样的通用键,通用键不能修改;而id则是自定义的键,自定义的键是可以自己设定和增加的,例如yyyy这些。这些从url里解析出来的键值对都会保存到RouteData.Values里。当站点项目里增加了Area之后,并在defaults中传入了area键值,area也可以认为是类似保留字这样的通用键,不但RouteData.Values里要保存,RouteData.DataTokens里也要保存。只有这样,路由才会正确解析area。另外Namespaces这样的约束键(constraints里的)也要保存到DataTokens里。

     如果我们自定义的域名路由解析类,能很好的解析代表域名的路由规则串,并将对应的键值对保存到正确的RouteData中,注册到路由集合中后,就能正确进行域名解析了。域名解析类的具体实现请参考文章开头提到的两篇文章,我这里只说改进的地方。

3、二级域名注册Area的问题及解决方法    

     从上面的知识点得知,我们可以使用下面的域名规则串来进行二级域名的路由注册了:

{controller}.web.com -- 使用controller控制二级域名
{area}.web.com -- 使用area控制二级域名

      为了实现二级域名注册area,在上面简单实现的例子里增加一个路由规则来解析二级域名(注意路由名称不能重复):

routes.Add("areaforweb", new DomainRoute(
                "{area}.web.com",                                                    
                "{controller}/{action}/{id}",                                        
                new { area = "Test", controller = "Page", action = "Index", id = "" }   
            ));

     程序运行后,很好,完美的解决了对 test.web.com/Page/Index 路径的解析,可还有什么问题吗?

     1、当我们进入二级域名的页面时,在使用ActionLink这样的方法生成二级域名的链接地址是可以的,但是要生成顶级域名下的链接地址就有问题了

     2、同理,当我们在顶级域名的页面上要使用ActionLink这样的方法生成二级域名的链接地址也是有问题的

     产生这些问题的原因在哪呢?我们得回过头去看看:

     RouteBase类的GetVirtualPath方法是实现从RouteData到url的还原,而且它只还原url的path部分,不还原host的部分,如果传入了多余的host的参数,生成的path部分就会有问题。因此在我们自定义的DomainRoute类中,在实现GetVirtualPath方法时,需要把保存在RouteData中的域名解析出来的键值对给去掉,类里面是通过RemoveDomainTokens方法实现的。

     在DomainRoute类中实现了对域名的解析,因此对应的我们还需要增加一个还原域名的方法,类里面是通过GetDomainData方法实现的。方法的原始实现方法是将域名规则串中用"{}"扩起来的部分的键在RouteData中找到其对应的值去替换。这就导致了我们上面1,2点问题的产生。

     当我们在二级域名的页面,使用的是二级域名的解析规则,还原域名时,域名中“{area}”部分始终会被“test”代替,无法回到顶级域名的部分;而当我们在顶级域名的页面时,使用的是顶级域名的解析规则,还原域名时,即使传入了像 area=“test" 的值,因为域名串中没有“{}”括起来的部分,所以始终会返回www.web.com,而无法得到去二级域名的链接地址。

     怎么解决呢?我想的是:顶级域名能不能用area="www"或者area=""来进行匹配?解析域名串,往Values和DataTokens写入数据是现成的,完全没有问题;我们需要修改的是GetDomainData方法,让它判断一下area="www"或者area=""时,还原时域名进行特殊的处理。修改后的方法如下:

public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
        {
            // 获得主机名
            string hostname = Domain;
            foreach (KeyValuePair<string, object> pair in values)
            {
                if (pair.Key == "area" && string.IsNullOrEmpty(pair.Value.ToString()))
                {
                    hostname = hostname.Replace("{" + pair.Key + "}", "www");
                }
                else
                {
                    hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
                }
            }

            //如果域名的area还没有被替换,说明路由数据里没有area,恢复为顶级域名
            if (hostname.Contains("{area}"))
            {
                hostname = hostname.Replace("{area}", "www");
            }

            RemoveDomainTokens(values);

            // Return 域名数据
            return new DomainData
            {
                Protocol = "http",
                HostName = hostname,
                Fragment = ""
            };
        }
View Code

     我们还需要一个重写的生成链接的ActionLink方法,这个方法在LinkExtensions类里,是HtmlHelper的一个自定义的扩展方法,修改后的方法如下:

public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool requireAbsoluteUrl)
        {
            if (requireAbsoluteUrl)
            {
                HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current);
                RouteData routeData = RouteTable.Routes.GetRouteData(currentContext);

                routeData.Values["controller"] = controllerName;
                routeData.Values["action"] = actionName;
                //如果不需要变换area,则routeValues不传入area的值
                if (routeValues.Keys.Contains("area"))
                {
                    routeData.Values["area"] = routeValues["area"];                 
                }

                DomainRoute domainRoute = routeData.Route as DomainRoute;
                if (domainRoute != null)
                {
                    DomainData domainData = domainRoute.GetDomainData(new RequestContext(currentContext, routeData), routeData.Values);
                    return htmlHelper.ActionLink(linkText, actionName, controllerName, domainData.Protocol, domainData.HostName, domainData.Fragment, routeData.Values, null);
                }
            }
            return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
        }
View Code

    一般我们生成的链接地址在需要跳转到其他area的时候,才在routeValues参数中传入area的值,否则不需要传入。另外,requireAbsoluteUrl参数对应是否需要生成带域名的地址,即绝对地址。调用方式如下:

@Html.ActionLink("前往", "Index", "Page", new { area = "Test" }, null, true)

     另外,我将原始方法的返回值从string改成了MvcHtmlString。否则返回值是string时,还得加上一层壳:

@Html.Raw(@Html.ActionLink("前往", "Index", "Page", new { area = "Test" }, null, true))

     至此,我们的问题基本上都解决了。在global中我们仅需要注册一个路由即可,默认值是顶级域名的首页。在项目的Area部分的TestAreaRegistration的RegisterArea方法中,注册area路由的内容就可以注销了。

routes.Add("web", new DomainRoute(
                "{area}.web.com",                                                    
                "{controller}/{action}/{id}",                                        
                new { area = "", controller = "Web", action = "Index", id = "" }   
            ));

 
     对于本机调试来说,不但需要设置hosts文件,而且在IIS站点上,需要把顶级域名和二级域名都绑定到站点上,如下图:

IIS设置

 

     这里是源代码下载。

     最后再说一下:在我的虚拟空间里,站点上只能绑定顶级域名,空间提供商限制了只能输入除www之外的域名部分。虽然可以用www.test.web.com这样的域名来实现二级域名,可实在有点不符合使用习惯。而且二级子域名也只能绑定三个。我哭啊……果然买的不如卖的精……

     最后的最后,对于这个功能其实老赵也有一系列的文章来进行解说和实现,给个地址:http://www.cnblogs.com/JeffreyZhao/archive/2009/08/25/url-routing-with-domain.html,从这个地址里可以把整个系列看完。他的实现方式更偏向于使用MVC的源代码进行实现,相对优雅不少。非常赞。大家也可以去研究一下。

posted @ 2014-03-13 16:10  芈老头  阅读(11406)  评论(6编辑  收藏  举报