ASP.NET Core Razor生成Html靜態文件

1、前言

最近作項目的時候,使用Util進行開發,使用Razor寫前端頁面。初次使用感受仍是不大習慣,以前都是先後端分離的方式開發的,可是使用Util封裝後的Angular後,感受開發效率仍是槓槓滴。html

2、問題

在發佈代碼的時候,Webpack打包異常,提示是缺乏了某些Html文件,我看了下相應的目錄,發現目錄缺乏了部分Html文件,而後就問了何鎮汐大大,給出的解決方案是,每一個頁面都須要訪問一下才能生成相應的Html靜態文件。這時候就產生了疑慮,是否有一種方式能獲取全部路由,而後只需訪問一次便可生成全部的Html頁面。前端

3、解決方案

3.1 每次訪問生成Html

解決方案思路:git

  • 繼承ActionFilterAttribute特性,重寫執行方法
  • 訪問的時候判斷訪問的Result是否ViewResult,若是是方可生成Html
  • 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

解決方案思路:github

  • 獲取全部已註冊的路由
  • 獲取使用RazorHtml自定義特性的路由
  • 忽略Api接口的路由
  • 構建RouteData信息,用於在RazorViewEngine中查找到相應的視圖
  • 構建ViewContext用於渲染出Html字符串
  • 將渲染獲得的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靜態文件api

/// <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();
}

4、使用方式

  • MVC控制器配置
    app

  • Startup配置
    前後端分離

  • 一次性生成方式,調用一次接口便可
    async

5、源碼地址

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/Razorside

6、參考

獲取全部已註冊的路由:https://github.com/kobake/AspNetCore.RouteAnalyzer

相關文章
相關標籤/搜索