最近有這么個需求:在一個站點上綁定多個域名,每個域名進去后都要進入不同的頁面。實現了這個功能以后,對於有多個域名,且有虛擬空間,但是虛擬空間卻只匹配有一個站點的用戶來說,可以節省很多小錢錢。
很久以前看過《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的源代碼進行實現,相對優雅不少。非常贊。大家也可以去研究一下。