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

很久以前看过《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 = "" }
));

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 = ""
};
}

我们还需要一个重写的生成链接的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);
}

一般我们生成的链接地址在需要跳转到其他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站点上,需要把顶级域名和二级域名都绑定到站点上,如下图:

这里是源代码下载。

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

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

MVC利用Routing实现多域名绑定一个站点、二级域名以及二级域名注册Area的更多相关文章

  1. win2003 多域名绑定一个ip

    一个IP绑定多个域名 很多虚拟主机,只有一个IP,很多个域名都指向该IP,但都能访问自己域名所在 的网站的内容,这就是一个IP绑定多个域名的技术. 我们得先了解一个概念 什么是主机头所谓的主机头的叫法 ...

  2. 关于宝塔一个站点绑定多个域名宝塔ssl证书的问题

    目前“宝塔SSL”自动申请绑定一个证书,即根域名和www域名,如果还需要绑定手机端m则需要绑定多个域名如果多域名绑定一个网站数据,需要新建多个站点指向同一文件目录. 用相同的方法,在不新建站点的前提下 ...

  3. nginx反向代理+tomcat域名绑定

    今天在用nginx做反向代理时,由于一个tomcat下有多个应用,因此要在tomcat做域名绑定.tomcat启动后,通过域名+端口是可以访问到页面的,但是通过nginx转发后就不能访问了,因此tom ...

  4. Nginx技巧:灵活的server_name,Nginx配置一个服务器多个站点 和 一个站点多个二级域名

    http://www.cnblogs.com/buffer/archive/2011/08/17/2143514.html Nginx强大的正则表达式支持,可以使server_name的配置变得很灵活 ...

  5. 万网免费主机wordpress快速建站教程-域名绑定及备案

    进入主机管理界面,点击管理 点击域名绑定,绑定域名项选择已有域名,选择已购买的域名,点击一键解析域名,点击添加,即可完成域名解析工作. 由于没有备案,备案状态显示为未备案,点击旁边的备案链接,跳转至阿 ...

  6. APACHE如何里一个站点绑定多个域名?用ServerAlias

    APACHE2如何里一个站点绑定多个域名?用ServerAlias以前很笨,要使多个域名指向同一站点总是这样写: <VirtualHost *:80>ServerAdmin i@kuigg ...

  7. APACHE如何里一个站点绑定多个域名?用ServerAlias servername

    APACHE2如何里一个站点绑定多个域名?用ServerAlias以前很笨,要使多个域名指向同一站点总是这样写: <VirtualHost *:80>ServerAdmin i@kuigg ...

  8. 为阿里云ECS服务器二级域名绑定tomcat子目录,实现一个IP多个二级域名

    摘要:前几天租了阿里云ECS服务器,选择的Windows系统,并在服务器上部署了tomcat服务器,随后我又买了一个域名,可一个域名只能指向一个IP地址,包括二级域名也只能指向一个IP地址,并不能指向 ...

  9. APACHE如何一个站点绑定多个域名?

    大家肯定遇到过这样的情况,需要APACHE2里一个站点绑定多个域名,那么如何操作呢?用ServerAlias 以前很笨,要使多个域名指向同一站点总是这样写: ServerAdmin admin@dom ...

随机推荐

  1. WebSocket 服务器4

    Java Websocket实例 Websocket   2015-04-11 14:11:54 发布 您的评价:       4.4   收藏     6收藏 介绍 现很多网站为了实现即时通讯,所用 ...

  2. Linux命令学习

    Linux命令学习 Ubuntu常用快捷键 •Ctrl+Alt+T: 打开终端 •Ctrl+Shift+T: 新建标签页 •Tab: 终端中命令补全 •Alt+数字N: 终端中切换到第N个标签页 •↑ ...

  3. SpringMVC实现Restful风格的WebService

    1.环境 JDK7 MyEclipse2014 tomcat8 maven 3.3.3 spring4.1.4 2.创建maven工程 使用MyEclipse创建maven工程的方式可以参考这篇博文( ...

  4. 第一节Unity3D简介

    Unity是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏.建筑可视化.实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎.Unity ...

  5. 每天一个linux命令(37):free 命令

    free命令可以显示Linux系统中空闲的.已用的物理内存及swap内存,及被内核使用的buffer.在Linux系统监控的工具中,free命令是最经常使用的命令之一. 1.命令格式: free [参 ...

  6. JS高级设计第七章——复习知识点

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  7. poj3580 伸展树(区间翻转 区间搬移 删除结点 加入结点 成段更新)

    好题.我做了很久,学了大牛们的区间搬移.主要的代码都有注释. #include<cstdio> #include<cstring> #include<iostream&g ...

  8. 【Gym 100015B】Ball Painting

    题 There are 2N white balls on a table in two rows, making a nice 2-by-N rectangle. Jon has a big pai ...

  9. 【CodeForces 618C】Constellation

    题 Cat Noku has obtained a map of the night sky. On this map, he found a constellation with n stars n ...

  10. SourceTree&Git部分名词解释

    SourceTree&Git部分名词解释 克隆(clone):从远程仓库URL加载创建一个与远程仓库一样的本地仓库 提交(commit):将暂存文件上传到本地仓库(我们在Finder中对本地仓 ...