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的源代碼進行實現,相對優雅不少。非常贊。大家也可以去研究一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM