一、前言
最近做項目的時候,使用Util進行開發,使用
Razor
寫前端頁面。初次使用感覺還是不大習慣,之前都是前後端分離的方式開發的,但是使用Util封裝後的
Angular
後,感覺開發效率還是杠杠滴。
二、問題
在釋出代碼的時候,
Webpack
打包異常,提示是缺少了某些
Html
檔案,我看了下相應的目錄,發現目錄缺少了部分Html檔案,然後就問了何鎮汐,給出的解決方案是,每個頁面都需要通路一下才能生成相應的Html靜态檔案。這時候就産生了疑慮,是否有一種方式能擷取所有路由,然後隻需通路一次即可生成所有的Html頁面。
三、解決方案
3.1 每次通路生成Html
解決方案思路:
- 繼承
特性,重寫執行方法ActionFilterAttribute
- 通路的時候判斷通路的
是否Result
,如果是方可生成HtmlViewResult
- 從
中查找到RazorViewEngine
後進行渲染View
/// <summary>
/// 生成Html靜态檔案
/// </summary>
public class HtmlAttribute : ActionFilterAttribute {
/// <summary>
/// 生成路徑,相對根路徑,範例:/Typings/app/app.component.html
/// </summary>
public string Path { get; set; }
/// <summary>
/// 路徑模闆,範例:Typings/app/{area}/{controller}/{controller}-{action}.component.html
/// </summary>
public string Template { get; set; }
/// <summary>
/// 執行生成
/// </summary>
public override async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next ) {
await WriteViewToFileAsync( context );
await base.OnResultExecutionAsync( context, next );
}
/// <summary>
/// 将視圖寫入html檔案
/// </summary>
private async Task WriteViewToFileAsync( ResultExecutingContext context ) {
try {
var html = await RenderToStringAsync( context );
if( string.IsNullOrWhiteSpace( html ) )
return;
var path = Util.Helpers.Common.GetPhysicalPath( string.IsNullOrWhiteSpace( Path ) ? GetPath( context ) : Path );
var directory = System.IO.Path.GetDirectoryName( path );
if( string.IsNullOrWhiteSpace( directory ) )
return;
if( Directory.Exists( directory ) == false )
Directory.CreateDirectory( directory );
File.WriteAllText( path, html );
}
catch( Exception ex ) {
ex.Log( Log.GetLog().Caption( "生成html靜态檔案失敗" ) );
}
}
/// <summary>
/// 渲染視圖
/// </summary>
protected async Task<string> RenderToStringAsync( ResultExecutingContext context ) {
string viewName = "";
object model = null;
if( context.Result is ViewResult result ) {
viewName = result.ViewName;
viewName = string.IsNullOrWhiteSpace( viewName ) ? context.RouteData.Values["action"].SafeString() : viewName;
model = result.Model;
}
var razorViewEngine = Ioc.Create<IRazorViewEngine>();
var tempDataProvider = Ioc.Create<ITempDataProvider>();
var serviceProvider = Ioc.Create<IServiceProvider>();
var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
var actionContext = new ActionContext( httpContext, context.RouteData, new ActionDescriptor() );
using( var stringWriter = new StringWriter() ) {
var viewResult = razorViewEngine.FindView( actionContext, viewName, true );
if( viewResult.View == null )
throw new ArgumentNullException( $"未找到視圖: {viewName}" );
var viewDictionary = new ViewDataDictionary( new EmptyModelMetadataProvider(), new ModelStateDictionary() ) { Model = model };
var viewContext = new ViewContext( actionContext, viewResult.View, viewDictionary, new TempDataDictionary( actionContext.HttpContext, tempDataProvider ), stringWriter, new HtmlHelperOptions() );
await viewResult.View.RenderAsync( viewContext );
return stringWriter.ToString();
}
}
/// <summary>
/// 擷取Html預設生成路徑
/// </summary>
protected virtual string GetPath( ResultExecutingContext context ) {
var area = context.RouteData.Values["area"].SafeString();
var controller = context.RouteData.Values["controller"].SafeString();
var action = context.RouteData.Values["action"].SafeString();
var path = Template.Replace( "{area}", area ).Replace( "{controller}", controller ).Replace( "{action}", action );
return path.ToLower();
}
}
3.2 一次通路生成所有Html
- 擷取所有已注冊的路由
- 擷取使用
自定義特性的路由RazorHtml
- 忽略Api接口的路由
- 建構
資訊,用于在RouteData
中查找到相應的視圖RazorViewEngine
-
用于渲染出Html字元串ViewContext
- 将渲染得到的Html字元串寫入檔案
擷取所有注冊的路由,此處是比較重要的,其他地方也可以用到。
/// <summary>
/// 擷取所有路由資訊
/// </summary>
/// <returns></returns>
public IEnumerable<RouteInformation> GetAllRouteInformations()
{
List<RouteInformation> list = new List<RouteInformation>();
var actionDescriptors = this._actionDescriptorCollectionProvider.ActionDescriptors.Items;
foreach (var actionDescriptor in actionDescriptors)
{
RouteInformation info = new RouteInformation();
if (actionDescriptor.RouteValues.ContainsKey("area"))
{
info.AreaName = actionDescriptor.RouteValues["area"];
}
// Razor頁面路徑以及調用
if (actionDescriptor is PageActionDescriptor pageActionDescriptor)
{
info.Path = pageActionDescriptor.ViewEnginePath;
info.Invocation = pageActionDescriptor.RelativePath;
}
// 路由屬性路徑
if (actionDescriptor.AttributeRouteInfo != null)
{
info.Path = $"/{actionDescriptor.AttributeRouteInfo.Template}";
}
// Controller/Action 的路徑以及調用
if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
if (info.Path.IsEmpty())
{
info.Path =
$"/{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
}
var controllerHtmlAttribute = controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute<RazorHtmlAttribute>();
if (controllerHtmlAttribute != null)
{
info.FilePath = controllerHtmlAttribute.Path;
info.TemplatePath = controllerHtmlAttribute.Template;
}
var htmlAttribute = controllerActionDescriptor.MethodInfo.GetCustomAttribute<RazorHtmlAttribute>();
if (htmlAttribute != null)
{
info.FilePath = htmlAttribute.Path;
info.TemplatePath = htmlAttribute.Template;
}
info.ControllerName = controllerActionDescriptor.ControllerName;
info.ActionName = controllerActionDescriptor.ActionName;
info.Invocation = $"{controllerActionDescriptor.ControllerName}Controller.{controllerActionDescriptor.ActionName}";
}
info.Invocation += $"({actionDescriptor.DisplayName})";
list.Add(info);
}
return list;
}
生成Html靜态檔案
/// <summary>
/// 生成Html檔案
/// </summary>
/// <returns></returns>
public async Task Generate()
{
foreach (var routeInformation in _routeAnalyzer.GetAllRouteInformations())
{
// 跳過API的處理
if (routeInformation.Path.StartsWith("/api"))
{
continue;
}
await WriteViewToFileAsync(routeInformation);
}
}
/// <summary>
/// 渲染視圖為字元串
/// </summary>
/// <param name="info">路由資訊</param>
/// <returns></returns>
public async Task<string> RenderToStringAsync(RouteInformation info)
{
var razorViewEngine = Ioc.Create<IRazorViewEngine>();
var tempDataProvider = Ioc.Create<ITempDataProvider>();
var serviceProvider = Ioc.Create<IServiceProvider>();
var routeData = new RouteData();
if (!info.AreaName.IsEmpty())
{
routeData.Values.Add("area", info.AreaName);
}
if (!info.ControllerName.IsEmpty())
{
routeData.Values.Add("controller", info.ControllerName);
}
if (!info.ActionName.IsEmpty())
{
routeData.Values.Add("action", info.ActionName);
}
var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var viewResult = razorViewEngine.FindView(actionContext, info.ActionName, true);
if (!viewResult.Success)
{
throw new InvalidOperationException($"找不到視圖模闆 {info.ActionName}");
}
using (var stringWriter = new StringWriter())
{
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary, new TempDataDictionary(actionContext.HttpContext, tempDataProvider), stringWriter, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
/// <summary>
/// 将視圖寫入檔案
/// </summary>
/// <param name="info">路由資訊</param>
/// <returns></returns>
public async Task WriteViewToFileAsync(RouteInformation info)
{
try
{
var html = await RenderToStringAsync(info);
if (string.IsNullOrWhiteSpace(html))
return;
var path = Utils.Helpers.Common.GetPhysicalPath(string.IsNullOrWhiteSpace(info.FilePath) ? GetPath(info) : info.FilePath);
var directory = System.IO.Path.GetDirectoryName(path);
if (string.IsNullOrWhiteSpace(directory))
return;
if (Directory.Exists(directory) == false)
Directory.CreateDirectory(directory);
File.WriteAllText(path, html);
}
catch (Exception ex)
{
ex.Log(Log.GetLog().Caption("生成html靜态檔案失敗"));
}
}
protected virtual string GetPath(RouteInformation info)
{
var area = info.AreaName.SafeString();
var controller = info.ControllerName.SafeString();
var action = info.ActionName.SafeString();
var path = info.TemplatePath.Replace("{area}", area).Replace("{controller}", controller).Replace("{action}", action);
return path.ToLower();
}
四、使用方式
- MVC控制器配置
ASP.NET Core Razor生成Html靜态檔案 - Startup配置
ASP.NET Core Razor生成Html靜态檔案 - 一次性生成方式,調用一次接口即可
ASP.NET Core Razor生成Html靜态檔案
五、源碼位址
Util
Bing.NetCore
Razor生成靜态Html檔案:https://github.com/dotnetcore/Util/tree/master/src/Util.Webs/Razors 或者 https://github.com/bing-framework/Bing.NetCore/tree/master/src/Bing.Webs/Razors
六、參考
擷取所有已注冊的路由:https://github.com/kobake/AspNetCore.RouteAnalyzer