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

很久以前看过《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. 把字符串添加到HashMap中

    &ZhuoTai_Name=205&NoSongDanDish=0&OrderZhuoTai_ID=aca87b77797e4c859a53c228471a2636&Z ...

  2. [CareerCup] 11.3 Search in Rotated Sorted Array 在旋转有序矩阵中搜索

    11.3 Given a sorted array of n integers that has been rotated an unknown number of times, write code ...

  3. IOS开发之——保存图片到相册的功能实现

    //保存      UIButton *saveBtn = [[UIButton alloc] init];    //    saveBtn.frame = CGRectMake((screenWi ...

  4. 第七章 美化DetailView界面

    本项目是<beginning iOS8 programming with swift>中的项目学习笔记==>全部笔记目录 ------------------------------ ...

  5. C#Json序列化和反序列化

    1.动态决定数据是否要序列化 我的需求是这样的,我用了一款数据库的组件叫Dos.ORM,确实方便了不少,但是在用的时候,我发现一个问题,比如我定义的表中有一个字段添加时间,修改时间,这些都是默认的,在 ...

  6. python3 入门 (一) 基础语法

    1.编码问题 默认情况下,Python 3源码文件以 UTF-8 编码,所有字符串都是 unicode 字符串. 也可以为源码文件指定不同的编码,在文件头部加上: # coding=gbk 2.关键字 ...

  7. 如何添加win10命令提示符字体,美化显示效果

    如图,win10命令提示符的字体真让人难受的要死,难道就不能改成好看一点的吗?笔者开始了停不下来的百度和Google 在查找了大量零碎或者过时的资料后,终于找到了一篇看起来还是很靠谱的文章: Quic ...

  8. [BZOJ 2241][SDOI2011]打地鼠(枚举+预处理)

    题目:http://www.lydsy.com:808/JudgeOnline/problem.php?id=2241 分析: 鉴于R,C的取值很小,于是可以人为枚举R和C的大小,然后判定这个规格的锤 ...

  9. 四则运算 Day2

    元旦快乐篇 别人在过元旦,而我却在敲代码,说多了都是泪. 设计思路 1. 界面设计 程序运行时,跳出运行说明提示用户如何操作 用户阅读完说明后点击开始进入主界面,即操作界面,操作界面分为计时区,操作区 ...

  10. uva12532 线段树单点更新

    #include<stdio.h> #include<string.h> #include<queue> using namespace std; #define ...