文章目錄
- 為什麼要重寫 URL ?
- ASP.NET 2.0 中的原有的URL 對應
- 處理配置節
- 使用重寫的 URL 的參數
- 重寫 URL
- 在 web.config 中註冊重寫模組
- 使用重寫模組
- IIS 配置: 使用帶擴充的重寫模組代替 .aspx
- 總結
這篇文章描述了一個完整的 ASP.NET 2.0 URL 重寫方案。這個方案使用Regex來定義重寫規則並解決通過虛擬 URLs 訪問頁面產生回傳事件的一些可能的困難。
為什麼要重寫 URL ?
將 URL 重寫方法應用到你的 ASP.Net 應用程式的兩個主要原因是:可用性和可維護性。
可用性
誰都知道,相對於難於辨認的帶參數的長的查詢路徑,使用者更喜歡一些短的、簡潔的 URL。任何時候,一個容易記住和敲入的路徑比添加到收藏夾更有用。其次,當一個瀏覽器的收藏夾不可用時,記住的地址總比在搜尋引擎中輸入關鍵字進行搜尋,然後再尋找要強的多。比較下面的兩個地址:
(1) |
http://www.somebloghost.com/Blogs/Posts.aspx?Year=2006&Month=12&Day=10 |
(2) |
http://www. somebloghost.com/Blogs/2006/12/10/ |
第一個 URL 包含了查詢字串;第二個URL包含的資訊可以讓使用者清楚的看到他看的東西,它還可以使使用者更容易的修改地址欄的內容,如:http://www.somehost.com/Blogs/2006/12/.
可維護性
在很多WEB應用程式中,開發人員經常會將頁面從一個目錄移到另一個目錄,讓我們假設一開始有兩個可用頁面: http://www.somebloghost.com/Info/Copyright.aspx 和 http://www.somebloghost.com/Support/Contacts.aspx,但是後來開發人員將 Copyright.aspx 和 Contacts.aspx 移到了 Help 目錄,使用者收藏起來地址就需要重新置放。這個問題雖然可以簡單的用 Response.Redirect(new location) 來解決,但是如果有成百上千的頁面呢?應用程式中就會包含大量的無效連結。
使用 URL 重寫,允許使用者只需修改設定檔,這種方法可以讓開發人員將web應用程式邏輯結構與物理結構獨立開來。
ASP.NET 2.0 中的原有的URL 對應
ASP.NET 2.0 為 web 應用程式提供了一個開箱即用的映射靜態 URL 的解決方案。這個方案不用編寫代碼就可以在 web.config 中將舊的 URLs 映射到新的地址。 要使用 URL 對應,只需在 web.config 檔案的 system.web 節中建立一個新的 urlMappings 節 ,並添加要映射的地址 (“ ~/ ”指嚮應用程式的根目錄):
<urlMappings enabled="true">
<add url="~/Info/Copyright.aspx" mappedUrl="~/Help/Copyright.aspx" />
<add url="~/Support/Contacts.aspx" mappedUrl="~/Help/Contacts.aspx" />
</urlMappings>
這樣,如果使用者輸入 http://www.somebloghost.com/Support/Contacts.aspx, 它將看到 http://www.somebloghost.com/Help/Contacts.aspx , 而他並不知道那個頁已經移除。
這個方案對於只有兩個頁面被移到其它位置的情況是足夠的。但它對有一打的需要重定位的頁或者需要建立一個整潔的URL來說,它是不合適的。另一個使用Asp.Net 的原有的URL映射技術的不太好的地方是:如果 Contacts.aspx 頁包含的元素在回傳到伺服器時(這是非常可能的), 使用者將會驚奇的發現地址 http://www.somebloghost.com/Support/Contacts.aspx 卻變成了 http://www.somebloghost.com/Help/Contacts.aspx
。 這是因為ASP.NET 引擎用頁面的實際地址修改了表單form 的 action 屬性 ,所以表單就變成了下面的樣子:
<form name="formTest" method="post"
action="http://www.simple-talk.com/Help/Contacts.aspx" id="formTest">
</form>
這樣看來,URL 對應在ASP.NET 2.0 中幾乎是無用的。我們應當能夠使用一個映射規則來指定一系列相似的 URL。最好的解決方案就是使用Regex ( Wikipedia 上可以查看概覽,and 在 .NET 下的實現可以查看 MSDN), 但由於 ASP.NET 2.0 映射不支援Regex,所以我們需要開發一個內建到 URL 對應的不同的方案- URL 重寫模組。 最好的方法就是建立一個可重用的、簡單的配置模組來實現,顯然我們應建立一個 HTTP 模組 (關於 HTTP 模組的詳細資料請查看 MSDN 雜誌) 並在獨立的程式集中實現。要使這個程式集簡單易用,我們應實現這個重寫引擎的可配置性,即能夠在 web.config 中指定規則。
在開發過程中,我們應能使這個重寫模組開啟或關閉 (比如你有一個較難捕獲的bug,而它可能是由不正確的重寫模組引起的)這樣在 web.config 中對重寫模組的配置節進行開啟或關閉就成為一個選擇。這樣,在 web.config 中,一個配置節的樣本如下:
<rewriteModule>
<rewriteOn>true</rewriteOn>
<rewriteRules>
<rule source="(\d+)/(\d+)/(\d+)/"
destination="Posts.aspx?Year=$1&Month=$2&Day=$3"/>
<rule source="(.*)/Default.aspx"
destination="Default.aspx?Folder=$1"/>
</rewriteRules>
</rewriteModule>
這樣,所有像: http://localhost/Web/2006/12/10/ 這樣的請示,將會在內部將會用帶參數的請求重新導向到 Posts.aspx 。
請注意: web.config 是一個結構良好的 XML 檔案, 它禁止在屬性值中使用 & 符號,所以在例子中,應當使用 & 代替。
要在設定檔中使用這個重寫模組,還需要註冊節和指定處理模組,像下面這樣增加一個configSections配置節:
<configSections> <sectionGroup name="modulesSection">
<section name="rewriteModule" type="RewriteModule.
RewriteModuleSectionHandler, RewriteModule"/>
</sectionGroup>
</configSections>
這樣你就可以在 configSections 節的後面這樣使用了:
<modulesSection>
<rewriteModule>
<rewriteOn>true</rewriteOn>
<rewriteRules>
<rule source="(\d+)/(\d+)/(\d+)/" destination="Post.aspx?Year=$1&Month=$2&Day=$3"/>
<rule source="(.*)/Default.aspx" destination="Default.aspx?Folder=$1"/>
</rewriteRules>
</rewriteModule>
</modulesSection>
另一個我們在開發重寫模組過程中要做的就是還需要允許在虛擬路徑中傳遞參數,象這樣: http://www.somebloghost.com/2006/12/10/?Sort=Desc&SortBy=Date 。所以我們還需要有一個檢測通過虛擬 URL 傳遞參數的解決方案。
接下來讓我們來建立類庫。首先,我們要引用 System.Web 程式集,這樣我們可以實現一些基於 web 特殊功能。如果要使我們的模組能夠訪問 web.config,還需要引用 System.Configuration 程式集。
處理配置節
要能處理 web.config 中的配置,我們必需建立一個實現了 IConfigurationSectionHandler 介面的類 (詳情查看 MSDN )。如下:
using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Web;
using System.Xml;
namespace RewriteModule
{
public class RewriteModuleSectionHandler : IConfigurationSectionHandler
{
private XmlNode _XmlSection;
private string _RewriteBase;
private bool _RewriteOn;
public XmlNode XmlSection
{
get { return _XmlSection; }
}
public string RewriteBase
{
get { return _RewriteBase; }
}
public bool RewriteOn
{
get { return _RewriteOn; }
}
public object Create(object parent,
object configContext,
System.Xml.XmlNode section)
{
// set base path for rewriting module to
// application root
_RewriteBase = HttpContext.Current.Request.ApplicationPath + "/";
// process configuration section
// from web.config
try
{
_XmlSection = section;
_RewriteOn = Convert.ToBoolean(
section.SelectSingleNode("rewriteOn").InnerText);
}
catch (Exception ex)
{
throw (new Exception("Error while processing RewriteModule
configuration section.", ex));
}
return this;
}
}
}
RewriteModuleSectionHandler 類將在 web.config 中的 XmlNode 通過調用 Create 方法初始化。XmlNode 類的 SelectSingleNode 方法被用來返回模組的配置值。
使用重寫的 URL 的參數
在處理象 http://www. somebloghost.com/Blogs/gaidar/?Sort=Asc (這是一個帶參數的虛擬 URL ) 虛擬 URLS 時,能夠清楚的辨別通過虛擬路徑傳遞的參數是非常重要的,如下:
<rule source="(.*)/Default.aspx" destination="Default.aspx?Folder=$1"/>,
你可能使用這樣的 URL:
http://www. somebloghost.com/gaidar/?Folder=Blogs
它的效果和下面的相似:
http://www. somebloghost.com/Blogs/gaidar/
要處理這個問題,我們需要對'虛擬路徑參數' 進行封裝。這可以是通過一個靜態方法去訪問當前的參數集:
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Specialized;
using System.Web;
namespace RewriteModule
{
public class RewriteContext
{
// returns actual RewriteContext instance for
// current request
public static RewriteContext Current
{
get
{
// Look for RewriteContext instance in
// current HttpContext. If there is no RewriteContextInfo
// item then this means that rewrite module is turned off
if(HttpContext.Current.Items.Contains("RewriteContextInfo"))
return (RewriteContext)
HttpContext.Current.Items["RewriteContextInfo"];
else
return new RewriteContext();
}
}
public RewriteContext()
{
_Params = new NameValueCollection();
_InitialUrl = String.Empty;
}
public RewriteContext(NameValueCollection param, string url)
{
_InitialUrl = url;
_Params = new NameValueCollection(param);
}
private NameValueCollection _Params;
public NameValueCollection Params
{
get { return _Params; }
set { _Params = value; }
}
private string _InitialUrl;
public string InitialUrl
{
get { return _InitialUrl; }
set { _InitialUrl = value; }
}
}
}
可以看到,這樣就可以通過RewriteContext.Current 集合來訪問 “虛擬路徑參數”了,所有的參數都被指定成了虛擬路徑或頁面,而不是像查詢字串那樣了。
重寫 URL
接下來,讓我們嘗試重寫。首先,我們要讀取設定檔中的重寫規則。其次,我們要檢查那些在 URL 中與規則不符的部分,如果有,進行重寫並以適當的頁執行。
建立一個 HttpModule:
class RewriteModule : IHttpModule
{
public void Dispose() { }
public void Init(HttpApplication context)
{}
}
當我們添加 RewriteModule_BeginRequest 方法以處理不符合規則的 URL時,我們要檢查給定的 URL 是否包含參數,然後調用 HttpContext.Current.RewritePath 來進行控制並給出合適的 ASP.NET 頁。
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Configuration;
using System.Xml;
using System.Text.RegularExpressions;
using System.Web.UI;
using System.IO;
using System.Collections.Specialized;
namespace RewriteModule
{
class RewriteModule : IHttpModule
{
public void Dispose() { }
public void Init(HttpApplication context)
{
// it is necessary to
context.BeginRequest += new EventHandler(
RewriteModule_BeginRequest);
}
void RewriteModule_BeginRequest(object sender, EventArgs e)
{
RewriteModuleSectionHandler cfg =
(RewriteModuleSectionHandler)
ConfigurationManager.GetSection
("modulesSection/rewriteModule");
// module is turned off in web.config
if (!cfg.RewriteOn) return;
string path = HttpContext.Current.Request.Path;
// there us nothing to process
if (path.Length == 0) return;
// load rewriting rules from web.config
// and loop through rules collection until first match
XmlNode rules = cfg.XmlSection.SelectSingleNode("rewriteRules");
foreach (XmlNode xml in rules.SelectNodes("rule"))
{
try
{
Regex re = new Regex(
cfg.RewriteBase + xml.Attributes["source"].InnerText,
RegexOptions.IgnoreCase);
Match match = re.Match(path);
if (match.Success)
{
path = re.Replace(
path,
xml.Attributes["destination"].InnerText);
if (path.Length != 0)
{
// check for QueryString parameters
if(HttpContext.Current.Request.QueryString.Count != 0)
{
// if there are Query String papameters
// then append them to current path
string sign = (path.IndexOf('?') == -1) ? "?" : "&";
path = path + sign +
HttpContext.Current.Request.QueryString.ToString();
}
// new path to rewrite to
string rew = cfg.RewriteBase + path;
// save original path to HttpContext for further use
HttpContext.Current.Items.Add(
"OriginalUrl",
HttpContext.Current.Request.RawUrl);
// rewrite
HttpContext.Current.RewritePath(rew);
}
return;
}
}
catch (Exception ex)
{
throw (new Exception("Incorrect rule.", ex));
}
}
return;
}
}
}
這個方法必須註冊:
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(RewriteModule_BeginRequest);
}
但這些僅僅完成了一半,因為重寫模組還要處理表單的回傳和虛擬路徑參數集合,而這段代碼中你會發現並沒處理這些。讓我們先把虛擬路徑參數放到一邊,先來正確地處理最主要的回傳。
如果我們運行上面的代碼,並通過查看 ASP.NET 的 HTML 原始碼 的 action 會發現,它竟然包含了一個 ASP.NET 的實際路徑頁。例如,我們使用頁 ~/Posts.aspx 來處理像 http://www. somebloghost.com/Blogs/2006/12/10/Default.aspx 的請求, 發現 action="/Posts.aspx"。這意味著使用者並沒有使用虛擬路徑進行回傳,而是使用了實際的 http://www. somebloghost.com/Blog.aspx. 這個並不是我們需要的。所以,需要加一段代碼來處理這些不希望的結果。
首先,我們要在 HttpModule 註冊和實現一個另外的方法:
public void Init(HttpApplication context)
{
// it is necessary to
context.BeginRequest += new EventHandler(
RewriteModule_BeginRequest);
context.PreRequestHandlerExecute += new EventHandler(
RewriteModule_PreRequestHandlerExecute);
}
void RewriteModule_PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if ((app.Context.CurrentHandler is Page) &&
app.Context.CurrentHandler != null)
{
Page pg = (Page)app.Context.CurrentHandler;
pg.PreInit += new EventHandler(Page_PreInit);
}
}
這個方法檢查使用者是否請求了一個正常的 ASP.NET 頁,然後為該頁的 PreInit 事件增加處理過程。這兒 RewriteContext 將處理實際參數,然後二次重寫URL。二次重寫是必需的,以使 ASP.NET 能夠在它的表單的action屬性中使用一個虛擬路徑。
void Page_PreInit(object sender, EventArgs e)
{
// restore internal path to original
// this is required to handle postbacks
if (HttpContext.Current.Items.Contains("OriginalUrl"))
{
string path = (string)HttpContext.Current.Items["OriginalUrl"];
// save query string parameters to context
RewriteContext con = new RewriteContext(
HttpContext.Current.Request.QueryString, path);
HttpContext.Current.Items["RewriteContextInfo"] = con;
if (path.IndexOf("?") == -1)
path += "?";
HttpContext.Current.RewritePath(path);
}
}
最後,我們來看一下在我們的重寫模組程式集中的三個類:
在 web.config 中註冊重寫模組
要使用重寫模組,需要在設定檔中的 httpModules 節註冊重寫模組,如下:
<httpModules>
<add name="RewriteModule" type="RewriteModule.RewriteModule, RewriteModule"/>
</httpModules>
使用重寫模組
在使用重寫模組時,需要注意:
- 在 web.config 中來使用一些特殊字元是不可能的,因為它是一個結構良好的 XML 檔案,因此,你只能用 HTML 編碼的字元代替,如:使用 & 代替 &。
- 要在你的 ASPX 中使用相對路徑,需要在HTML標籤調用 ResolveUrl 方法,如: <img src="<%=ResolveUrl("~/Images/Test.jpg")%>" />。
- Bear in mind the greediness of regular expressions and put rewriting rules to web.config in order of their greediness, for instance:
<rule source="Directory/(.*)/(.*)/(.*)/(.*).aspx"
destination="Directory/Item.aspx?
Source=$1&Year=$2&ValidTill=$3&Sales=$4"/>
<rule source="Directory/(.*)/(.*)/(.*).aspx"
destination="Directory/Items.aspx?
Source=$1&Year=$2&ValidTill=$3"/>
<rule source="Directory/(.*)/(.*).aspx"
destination="Directory/SourceYear.aspx?
Source=$1&Year=$2&"/>
<rule source="Directory/(.*).aspx"
destination="Directory/Source.aspx?Source=$1"/>
- 如果你要在頁面中使用 RewriteModule 而不使用 .aspx,就必須在 IIS 中進行配置以使用期望的擴充映射到請求頁,如下節所述:
IIS 配置: 使用帶擴充的重寫模組代替 .aspx
要使用帶擴充的重寫模組代替 .aspx (如 .html or .xml), 必須配置 IIS ,以使這些擴充映射到 ASP.NET 引擎 (ASP.NET ISAPI 擴充)。要進行這些設定,需要以管理員身份登入。
開啟 IIS 管理主控台,並選擇你要配置的網站的虛擬路徑:
Windows XP (IIS 5)
Virtual Directory "RW"
Windows 2003 Server (IIS 6)
Default Web Site
然後在虛擬路徑標籤上點擊 Configuration… 按鈕 (或如果要使用整個網站都做映射就選擇主目錄標籤)。
Windows XP (IIS 5)
Windows 2003 Server (IIS 6)
接下來,點擊添加按鈕,並輸入一個擴充,你還需要指定一個 ASP.NET ISAPI 擴充,注意去掉選項的對勾以檢查檔案是否存在。
如果你要把所有的擴充都映射到 ASP.NET,對Windows XP上的 IIS 5 來說只需要設定 .* 到 ASP.NET ISAPI ,但對 IIS 6 就不一樣了,點擊“添加”然後指定 ASP.NET ISAPI 擴充。
總結
現在,我們已經建立了一個簡單的但非常強大的 ASP.NET 重寫模組,它支援可基於Regex的 URLs 和頁面回傳,這個解決方案是容易實現的,並且提供給使用者的例子也是可用的,它可以用簡短的、整潔的URL來替代查詢字串參數。 要使用這個模組,只需簡單在你的應用程式中對 RewriteModule 進行引用,然後在 web.config 檔案中添加幾行代碼以使你不想顯示的 URL 通過Regex代替。這個重寫模組是很容易部署的,因為只需要在web.config中修改任何“虛擬”的URL即可,如果你需要進行測試,還可以對重寫模組進行關閉。
要想對重寫模組有一個深入的瞭解,你可以查看本文提供的原代碼。我相信你會發現這是一個比ASP.NET提供的原始映射更好的體驗。