系列回顧
- 《ASP.NET Core 中的SEO優化(1):中間件實現服務端靜態化緩存》
- 《ASP.NET Core 中的SEO優化(2):中間件中渲染Razor視圖》
- 《ASP.NET Core 中的SEO優化(3):自定義路由匹配和生成》
背景
切換主題,是博客、CMS等系統的必備功能,一般來說,有三種切換主題的需求。
- 在管理后台上傳主題包,並選擇主題
- 前端自動按照頻道、欄目等切換模版
- 用戶在前端切換主題,並記錄用戶的選擇
這三種需求,其實核心原理都是一樣,就是制定一套主題的目錄,切換主題等於切換目錄名。主題內的頁面模版都是按照一定的規則存放的。
下面是兩個主題包的目錄示例:
.
├── theme0
| ├── Assets
| | ├── js
| | ├── css
| | └── img
| ├── Home
| | ├── Index.cshtml
| | └── About.cshtml
| ├── Article
| | ├── Index.cshtml
| | └── Detail.cshtml
| └── Shared
| ├── Page.cshtml
| └── _Layout.cshtml
└── theme1
├── Assets
| ├── js
| ├── css
| └── img
├── Home
| ├── Index.cshtml
| └── About.cshtml
├── Article
| ├── Index.cshtml
| └── Detail.cshtml
└── Shared
├── Page.cshtml
└── _Layout.cshtml
大家一定注意到了,上面每個主題包里都按照傳統ASP.NET MVC的約定來划分目錄:控制器名為文件夾,操作名為視圖文件。其實這里只是方便起見,按照接下來介紹的方法,是可以完全地自定義這個目錄划分的。
原理
當ASP.NET MVC從控制器處理完數據返回視圖的時候,ASP.NET MVC會按照默認的多個路徑去查找文件,如果文件存在,則使用該文件渲染,如果不存在,則尋找下一個路徑,比如默認的路徑會有/{Area}/{Controller}/{Action}.cshtml、/{Controller}/{Action}.cshtml、/Shared/{Action}.cshtml等等我們熟悉的約定,那么在查找視圖文件時,會安裝從左往右的路徑去查詢,如果都查詢不出來,是會報錯的。
而如果要做到切換主題文件夾名來切換主題,我們就需要在默認規則上加主題的目錄占位符,使的查詢時用主題文件夾名來替換占位符,例如/{theme}/{Controller}/{Action}.cshtml、/{theme}/Shared/{Action}.cshtml等等,這樣,當查詢視圖文件時,就能匹配到對應的主題文件夾,並且找到相應的視圖了。
總結起來,切換主題功能有兩個重點需要我們去實現:
- 在原有規則中加入占位符
- 每次請求都獲取當前的主題名,並改變視圖查詢路徑
實現
最簡單的實現,在操作(action)的最后return View(viewPath)時傳入視圖路徑,直接就能指向對應視圖,但是,這樣做一點都不靈活,而且每個操作都要傳路徑也是不夠簡潔,不容易維護,所以我們需要更好的解決方案。
ASP.NET MVC 實現
在ASP.NET MVC時代,我們可通過繼承RazorViewEngine類,在基類的ViewLocationFormats和PartialViewLocationFormats兩個屬性中加入有主題目錄名占位符的路徑,並重寫CreateView、CreatePartialView、FileExists三個方法,使每次請求都能獲取最新的主題名,如下面的例子中從路由數據對象中獲取主題名:
public class TemplateViewEngine : RazorViewEngine
{
public TemplateViewEngine() : base()
{
ViewLocationFormats = new[] {
"~/Views/{1}%1/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",//默認路徑
"~/Views/Shared%1/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
};
PartialViewLocationFormats = new[] {
"~/Views/{1}%1/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",//默認路徑
"~/Views/Shared%1/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
};
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var template = controllerContext.RouteData.Values["template"] != null ? "/" + controllerContext.RouteData.Values["template"].ToString() : "";
return base.CreatePartialView(controllerContext, partialPath.Replace("%1", template));
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var template = controllerContext.RouteData.Values["template"] != null ? "/" + controllerContext.RouteData.Values["template"].ToString() : "";
return base.CreateView(controllerContext, viewPath.Replace("%1", template), masterPath);
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
var template = controllerContext.RouteData.Values["template"] != null ? "/" + controllerContext.RouteData.Values["template"].ToString() : "";
return base.FileExists(controllerContext, virtualPath.Replace("%1", template));
}
}
事實上,如果是需要實現不同用戶不同主題的功能,主題信息可以存儲在Session中,還能從controllerContext實例獲取Session中存儲的主題名。
那么,在ASP.NET Core中如何實現呢?
ASP.NET Core 實現
ASP.NET Core 相比ASP.NET MVC框架,雖然使用上為了開發者平滑過渡,很多約定都相同,但是架構本身是做了翻天覆地的重構和優化,得益於一脈相承的MSDI框架,ASP.NET Core框架實現了組件化,很多功能都通過IoC的方式修改或擴展。例如本文介紹的主題情況功能,就是實現IViewLocationExpander接口來達到擴展配置的目的,而且還比ASP.NET MVC的更加簡潔:
public class TemplateViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var template = context.Values["template"] ?? "Default";
string[] locations = { "/Views/" + template + "/{1}/{0}.cshtml", "/Views/" + template + "/{0}.cshtml", "/Views/" + template + "/Shared/{0}.cshtml" };
return locations.Union(viewLocations);
}
public void PopulateValues(ViewLocationExpanderContext context)
{
context.Values["template"] = context.ActionContext.RouteData.Values["Template"]?.ToString() ?? "Default";
}
}
這個接口里面,PopulateValues方法主要用來獲取實時的主題信息,context.ActionContext中除了RouteData可獲得實時數據,還有HttpContext實例可獲得用戶信息,甚至能利用RequestServices實例注入服務。而只有在PopulateValues中修改了context,ExpandViewLocations方法才會從context中獲得主題信息,從而達到修改視圖查找路徑的目的。
當我們實現了IViewLocationExpander接口后,還需要在Startup類的services.AddMvc();下修改MVC的配置:
services.AddMvc();
//配置模版視圖路徑
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new TemplateViewLocationExpander());
});
PS:這種修改MVC內部配置的方式很有趣,以后有空會研究一番。
總結
本文主要介紹了在ASP.NET Core中利用修改視圖查詢路徑實現主題切換的功能,雖然只介紹了核心部分,但是其它部分如管理主題、前端切換等功能,都是很容易實現的,以后我會在我的框架樣例中實現,敬請大家關注啦!
