前言
前兩篇文章主要總結了CMS系統兩個技術點在ASP.NET Core中的應用:
而本篇文章,繼續介紹另一個技術點:自定義路由匹配和生成。
背景
在MVC5時代,默認的路由可能就是簡單的約定/{controller}/{action}/{id}
,第一節對應控制器(Controller)名,第二節對應操作(Action)名,第三節是參數名。
在WebApi和ASP.NET Core時代,有了Route
特性來指定相應操作的路由指向,可以很靈活地配置RESTful Api。
但是,路由靈活性在注重SEO的CMS系統中有更苛刻的要求。例如:
- 欄目的列表 ->
/{父欄目名}/{子欄目名}-{頁碼}/
- 文章詳情頁 ->
/{欄目名}/{文章名}.html
- 標簽頁 ->
/{標簽名}
這些友好的url鏈接使用默認的路由約定是很難實現的,當然是可以配置路由規則去傳遞參數:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "article_list",
template: "{parentCategory}/{category}-{page}/",
defaults: new { controller = "Article", action = "Index" });
routes.MapRoute(
name: "article_detail",
template: "{category}/{article}.html",
defaults: new { controller = "Article", action = "Detail" });
routes.MapRoute(
name: "tags",
template: "{tag}/",
defaults: new { controller = "Article", action = "Tag" });
});
但是,這樣配置很繁瑣,也不靈活,如果還要傳入更多規則,比如不向下匹配,就顯得捉襟見肘了。那有沒有更靈活的方案呢?當然有,就是本文接下來要介紹的IRouter接口。
原理
上一節最后介紹的一般路由配置方法,其實是MapRoute
方法創建一個個IRouter
的實例,添加到IRouteBuilder
實例中,具體方法可以看看源碼。
所以我們可以實現一個自定義的IRouter
,就能不用默認的約定,去實現我們的苛刻需求。
首先要看看IRouter
這個接口的定義,源碼在aspnet/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs
namespace Microsoft.AspNetCore.Routing
{
public interface IRouter
{
Task RouteAsync(RouteContext context);
VirtualPathData GetVirtualPath(VirtualPathContext context);
}
}
實現
IRouter
接口只有兩個方法,本文分別給出代碼示例來介紹。
RouteAsync
這個方法中實現路由匹配,從路由上下文中獲取所需要的數據,存入RouteData字典中,再從控制器取出做相應的處理。
public async Task RouteAsync(RouteContext context)
{
var requestedUrl = context.HttpContext.Request.Path.Value.TrimStart('/').ToLower();
var split = requestedUrl.Split('/');
if (secoend != null && secoend.EndsWith(".html") && split.Length == 2)
{
var title = secoend.Replace(".html", "");
context.RouteData.Values["controller"] = "Article";
context.RouteData.Values["action"] = "Detail";
context.RouteData.Values["category"] = first;
context.RouteData.Values["title"] = title;
}
//...對請求路徑進行一系列的判斷
//最后注入`MvcRouteHandler`示例執行`RouteAsync`方法,表示匹配成功
await context.HttpContext.RequestServices.GetService<MvcRouteHandler>().RouteAsync(context);
}
這樣,當請求路由為/news/asp.net.html
時,就會匹配到上面的規則,請求進入Article
控制器中的Detail
操作被處理,並且可以從控制器中的RouteData.Values["category"]?.ToString();
方法拿到所需的數據。
GetVirtualPath
這個方法實現路由生成,可以從路由上下文中獲取RouteData字典中的數據,進行虛擬路徑(區別與真實目錄)的生成。
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
var path = string.Empty;
var hasController = context.Values.TryGetValue("controller", out var controller);
var hasAction = context.Values.TryGetValue("action", out var action);
var hasCategory = context.Values.TryGetValue("category", out var category);
var hasTitle = context.Values.TryGetValue("title", out var title);
if (hasController && hasAction && hasCategory && hasTitle)
{
path = $"/{category/{title}.html";
}
return path != string.Empty ? new VirtualPathData(this, path) : null;
}
這樣,當調用路徑生成方法@Url.Action("Detail","Article",new { title="asp.net", category="news" })
,就會生成"/news/asp.net.html"這樣的路徑來。
IRouter的設置生效
自定義實現的IRouter
如何設置到原有的MVC項目中呢?方法很簡單,上面已經簡單說道了,其實app.UseMvc
這個方法就有添加IRouter
的方法:
app.UseMvc(routes =>
{
//添加 自定義路由匹配與url生成組件
routes.Routes.Add(new RouteProvider());
});
這里RouteProvider
是IRouter
的實現,這樣自定義的路由提供對象就生效啦!
相關小技巧
-
SEO中會有一條鐵規則,就是不是有效鏈接的情況下只能返回404,比如,手動輸入了一個路徑,能匹配到文章詳情的操作(action),如果數據庫中查不出文章,本來應該不能匹配的,但是,如果在匹配的時候查詢數據庫確認是否存在的話,會加大系統的壓力,所以,可以放在操作(action)中查詢,查不到再返回
NotFound
,讓上一篇文章《ASP.NET Core 中的SEO優化(2):中間件中渲染Razor視圖》中介紹的中間件一並處理輸出404頁面。 -
站內鏈接如果使用帶域名的絕對路徑,能夠提高優化效果,我們可以自己寫一個
UrlHelper
的擴展方法:
public static class UrlHelperExtensions
{
public static string AbsoluteAction(
this IUrlHelper helper,
string actionName,
string controllerName,
object routeValues = null)
{
string scheme = helper.ActionContext.HttpContext.Request.Scheme;
return helper.Action(actionName, controllerName, routeValues, scheme);
}
public static string AbsoluteContent(
this IUrlHelper helper,
string contentPath)
{
return new Uri(helper.ActionContext.HttpContext.Request.GetUri(), helper.Content(contentPath)).ToString();
}
public static string AbsoluteRouteUrl(
this IUrlHelper helper,
string routeName,
object routeValues = null)
{
string scheme = helper.ActionContext.HttpContext.Request.Scheme;
return helper.RouteUrl(routeName, routeValues, scheme);
}
}
總結
本文主要介紹了自定義路由匹配和生成的解決方案,把路由相關的處理集中到一個類中,避免分散在各個視圖進行維護。下篇文章,將會介紹自定義視圖搜索目錄及主題切換。