ASP.NET MVC URL重寫與優化(進階篇)-繼承RouteBase玩轉URL
在初級篇中,我們介紹瞭如何利用基於ASP.NET MVC的Web程式中的Global檔案來簡單的重寫路由。也介紹了它本身的侷限性-依賴於路由資訊中的鍵值對:
如果鍵值對中沒有的值,我們無法將其利用湊出我們想要的URL表示式。
在進階篇中,我們將介紹ASP.NET 路由相關類的基類-抽象類RouteBase,並演示如何通過繼承它,讓URL重寫和優化變成Free Style。
一,老闆的需求
假設我們是手機銷售網站的一名程式猿(承接初級篇),經過第一次的URL重寫之後,我們的手機分類頁面的URL的改變:
http://www.xxx.com/category/showcategory?categoryid=0001&view=list&orderby=price&page=1
=>
http://www.xxx.com/category/0001
現在老闆又提出了新的需求,URL的語義化,從而更好的反應網站的結構:
http://www.xxx.com/ca-categoryname
比如Nokia是一個分類,那麼對應URL為 /ca-nokia,如果是iPhone分類,URL則對應 /ca-iphone。ca字首的意思是分類category。
對於這個需求簡單的配置Global檔案是無法做到的。首先我們來介紹一下ASP.NET 路由的所有類的基類RouteBase。
二,RouteBase類簡介與執行機制
1. RouteBase類位於System.Web.Routing名稱空間,結構如下:
public abstract class RouteBase
{
protected RouteBase();
public abstract RouteData GetRouteData(HttpContextBase httpContext);
public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
}
- GetRouteData:根據Http請求資訊返回一個物件-包含路由定義的值(如果該路由與當前請求匹配)或 null(如果該路由與請求不匹配)。
- GetVirtualPath:檢查路由值是否與某個規則匹配,返回一個物件(包含生成的 URL 和有關路由的資訊)或 null(如果路由與 values 不匹配)。
- RouteBase:初始化該類供繼承的類例項使用。此建構函式只能由繼承的類呼叫。
看完以上定義,可能大家會暈忽忽。我們來弄一個簡單的例子說明這幾個方法是如何運作的。
首先我們新建一個類庫JohnConnor.Routing,並且繼承抽象類RouteBase:
using System; using System.Collections.Generic; using System.Linq; using System.Text;
using System.Web.Mvc;//需要新增引用,請使用3.0以上版本 using System.Web.Routing; using JohnConnor.Models;
namespace JohnConnor.Routing { public class CategoryUrlProvider:RouteBase { public override RouteData GetRouteData(System.Web.HttpContextBase httpContext) { return null;//斷點1 } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { return null;//斷點2 } } }
這樣CategoryUrlProvider類就包含了用來處理路由對映的方法。
首先我們需要在Web程式中新增JohnConnor.Routing類庫的引用,然後我們把CategoryUrlProvider類註冊到Global檔案的路由表中。
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.Add(new JohnConnor.Routing.CategoryUrlProvider());//分類規則routes.MapRoute("Home", "", new { controller = "Home", action = "Index"});//主頁
}
這裡相當於添加了一條新的路由規則。重新生成一下Web程式在CategoryUrlProvider中打好斷點,F5啟動。
2. GetRouteData()方法
這時候相當與你在瀏覽器輸入了http//localhost:1234/<假設本地埠號是1234>,此時程式需要判斷這個URL匹配的是哪個路由值。
自上而下的匹配,首先會嘗試匹配我們新增的分類路由規則,此時會命中GetRouteData()方法中的斷點。
因為我們返回了null,意味著該請求與我們新增的分類路由規則不匹配,那程式將在路由表中繼續自上而下的進行匹配。
直到在主頁這一條規則中與其URL表示式匹配,獲取了對應的路由值-呼叫HomeController.Index()方法。
如果你把GetRouteData()方法修改一下:
public override RouteData GetRouteData(System.Web.HttpContextBase httpContext) { var data = new RouteData(this, new MvcRouteHandler()); data.Values.Add("controller", "Home"); data.Values.Add("action", "Index"); return data; }
你就會發現,無論你在http//localhost:1234/後面輸入任何相對URL,都會被定向到HomeController.Index()方法。
因為返回的是路由值而不是null,表示已經找到匹配項,就不會再往下匹配了。<這條規則覆蓋了後面所有的規則>
當然,請不要這樣寫。。。
由此可以推斷出GetRouteData()方法在路由對映中擔任的角色:處理請求中的URL,返回相應的路由值,不處理或不匹配則返回null。
3. VirtualPathData()方法
如果你在Razor頁面有這樣一段通過指定路由值來獲取URL的程式碼
<a href="@Url.Action("Index", "Home")">首頁</a>
當檢視引擎渲染頁面到這句程式碼時,HomeController.Index()方法會被解析為一個RouteValueDictionary型別的不分大小寫的鍵值對<假設鍵值對物件為values>:
values["controller"]="Home"; values["action"]="Index";
這個鍵值對錶示了一個路由值。
同樣是在路由表中自上而下的匹配這個路由值,嘗試第一條分類規則時,就會命中VirtualPathData()方法中的斷點。
我們返回一個null,表示不匹配,則程式進行下一個規則的匹配。
直到找到主頁規則的路由值與之匹配時,構造出相應的相對URL"",並返回該URL。
顯示為:
<a href="http://localhost:1234/">首頁</a>
如果我們也改寫一下VirtualPathData()方法:
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return new VirtualPathData(this, "This-is-a-Test-URL");
}
結果是你通過上述方法構造的URL不論請求來自哪裡,全部都會顯示成http://localhost:1234/This-is-a-Test-URL
因為我們返回的是一個相對路徑,而不是null,表示已經找到匹配項,則匹配不會往下繼續。<同上這條規則覆蓋了後面所有的規則>
再一次提示,請不要這樣寫。。。
由此可看出,VirtualPathData()在路由對映中的活:處理請求與路由鍵值對,生成相應URL,不處理或不匹配則返回null。
4.方法重寫的規則
在上文中,我一再的用紅色字型提示,請不要這樣寫。因為每一個URL的重寫類,建議僅僅處理儘可能少的路由對映。
比如CategoryUrlProvider僅處理CategoryController.Show(string categoeyid)這一個Action方法的對映。凡是不是這個方法相關的對映,都返回null。
繼續去匹配別的規則。
三,開始動手把~
為了最快的說明問題,我們簡化了網站的內容。以下內容有助於理解後面的程式,如果時間充裕,還是自己構建一個網站來嘗試以下。
首先我們在JohnConnor.Routing類庫中建立Category.cs來儲存分類模型,並把所有的分類顯示的儲存在List<Category>中,
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace JohnConnor.Models { //分類模型 public class Category { public string CategoeyID { get; set; } public string CategoeyName { get; set; } } public static class CategoryManager { //這裡只顯示建立了三個分類作為示例,實際中AllCategories可以從資料來源讀取。 public static readonly List<Category> AllCategories = new List<Category> { new Category(){ CategoeyID="001", CategoeyName="Nokia"}, new Category(){ CategoeyID="002", CategoeyName="iPhone"}, new Category(){ CategoeyID="003", CategoeyName="Anycall"} }; } }
假設我們網站的CategoryController是這樣的。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using JohnConnor.Models; namespace JohnConnor.Web.Controllers { public class CategoryController : Controller { public ActionResult ShowCategory(string id) { var category = CategoryManager.AllCategories.Find(c => c.CategoeyID == id); return View(category); } } }
首先我們建議,VirtualPathData()和GetRouteData()方法是成雙成對出現的。一旦你制定了一條路由規則,比如分類規則/ca-categoryname,那麼:
- GetRouteData()必須處理與這條規則匹配的每一條URL,返回相同的路由值;放棄與之不匹配的URL,返回null,讓匹配繼續。
- VirtualPathData()必須處理與這條規則匹配的每一次路由請求,返回相同的URL;放棄與之不匹配的請求,返回null,讓匹配繼續。
!!!兩者相輔相成的完成了路由值和URL的相互對映,漏掉一個,就不能構成一個完成的路由規則。直接結果是出現404或生成URL地址錯誤。
GetRouteData()的程式碼:
public override RouteData GetRouteData(System.Web.HttpContextBase httpContext) { var virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath + httpContext.Request.PathInfo;//獲取相對路徑
virtualPath = virtualPath.Substring(2).Trim('/');//此時URL會是~/ca-categoryname,擷取後面的ca-categoryname
if (!virtualPath.StartsWith("ca-"))//判斷是否是我們需要處理的URL,不是則返回null,匹配將會繼續進行。 return null; var categoryname = virtualPath.Split('-').Last();//擷取ca-字首後的分類名稱
//嘗試根據分類名稱獲取相應分類,忽略大小寫 var category = CategoryManager.AllCategories.Find(c => c.CategoeyName.Equals(categoryname,StringComparison.OrdinalIgnoreCase)); if (category == null)//如果分類是null,可能不是我們要處理的URL,返回null,讓匹配繼續進行 return null; //至此可以肯定是我們要處理的URL了 var data = new RouteData(this, new MvcRouteHandler());//宣告一個RouteData,新增相應的路由值 data.Values.Add("controller", "Category"); data.Values.Add("action", "ShowCategory"); data.Values.Add("id", category.CategoeyID); return data;//返回這個路由值將呼叫CategoryController.ShowCategory(category.CategoeyID)方法。匹配終止 }
VirtualPathData()的程式碼
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { //判斷請求是否來源於CategoryController.Showcategory(string id),不是則返回null,讓匹配繼續 var categoryId = values["id"] as string; if (categoryId == null)//路由資訊中缺少引數id,不是我們要處理的請求,返回null return null; //請求不是CategoryController發起的,不是我們要處理的請求,返回null if (!values.ContainsKey("controller") || !values["controller"].ToString().Equals("category",StringComparison.OrdinalIgnoreCase)) return null; //請求不是CategoryController.Showcategory(string id)發起的,不是我們要處理的請求,返回null if (!values.ContainsKey("action") || !values["action"].ToString().Equals("showcategory", StringComparison.OrdinalIgnoreCase)) return null; //至此,我們可以確定請求是CategoryController.Showcategory(string id)發起的,生成相應的URL並返回 var category = CategoryManager.AllCategories.Find(c => c.CategoeyID == categoryId); if (category == null) throw new ArgumentNullException("category");//找不到分類丟擲異常 var path = "ca-" + category.CategoeyName.Trim();//生成URL return new VirtualPathData(this, path.ToLowerInvariant()); }
至此,我們就把這條路由規則的對映處理完整了。如果你掌握了上述技術,任何的URL重寫和優化需求,我相信你都能Hold住。
如果我們的主頁頁面是這樣<Razor檢視引擎>:
@model List<JohnConnor.Models.Category> @{ ViewBag.Title = "主頁"; } <h2><a href="@Url.Action("Index", "Home")">首頁</a></h2> <p> @foreach (var item in Model) { <a href="@Url.Action("ShowCategory", "Category", new { id = item.CategoeyID })">@item.CategoeyName</a> } </p>
三個分類連線會得到這樣的結果
<a href="/ca-nokia">Nokia</a> <a href="/ca-iphone">iPhone</a> <a href="/ca-anycall">Anycall</a>
點選每一個連線都會先進入我們的處理程式,生成相應的路由值-呼叫CategoryController.Showcategory(string id)方法根據id顯示相應的分類頁面。