ASP.NET Core MVC 在过滤器ActionFilter中保存页面的生成的html静态页面文件


在大型网站系统中,为了提高系统访问性能,往往会把一些不经常变得内容发布成静态页,比如商城的产品详情页,新闻详情页,这些信息一旦发布后,变化的频率不会很高,如果还采用动态输出的方式进行处理的话,肯定会给服务器造成很大的资源浪费。但是我们又不能针对这些内容都独立制作静态页,所以我们可以在系统中利用伪静态的方式进行处理,至于什么是伪静态,大家可以百度下。我们这里就来介绍一下,在asp.net core mvc中实现伪静态的方式。

 

mvc框架中,view代表的是视图,它执行的结果就是最终输出到客户端浏览器的内容,包含html,css,js等。如果我们想实现静态化,我们就需要把view执行的结果保存成一个静态文件,保存到指定的位置上,比如磁盘、分布式缓存等,下次再访问就可以直接读取保存的内容,而不用再执行一次业务逻辑。那asp.net core mvc要实现这样的功能,应该怎么做?答案是使用过滤器,在mvc框架中,提供了多种过滤器类型,这里我们要使用的是动作过滤器,动作过滤器提供了两个时间点:动作执行前,动作执行后。我们可以在动作执行前,先判断是否已经生成了静态页,如果已经生成,直接读取文件内容输出即可,后续的逻辑就执行跳过。如果没有生产,就继续往下走,在动作执行后这个阶段捕获结果,然后把结果生成的静态内容进行保存。

我的ActionFilter

C# 全选
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using WebMarkupMin.Core;
using WebMarkupMin.NUglify;
using YESCMS.Libs;

namespace Microsoft.AspNetCore.Mvc
{
    /// <summary>
    /// 生成Html静态文件
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class HtmlStaticFileAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// 页面更新参数,用于更新页面,更新文件 如 https://localhost:44345/?__update__
        /// </summary>
        public static string UpdateFileQueryString = "__update__";
        /// <summary>
        /// 页面测试参数,测试页面,不更新文件 如  https://localhost:44345/?__test__
        /// </summary>
        public static string TestQueryString = "__test__";

        /// <summary>
        /// 是否压缩html
        /// </summary>
        public static bool IsHtmlCompression = true;

        /// <summary>
        /// 静态文件保存路径, 则默认放在 {wwwroot}\page-cache 文件夹下
        /// </summary>
        public static string OutputFolder = "page-cache";




        public override void OnActionExecuting(ActionExecutingContext context)
        {
            // 如果强制更新 或者 测试
            if (IsUpdateOutputFile(context) || IsTest(context))
            {
                base.OnActionExecuting(context);
                return;
            }

            string filePath = GetFilePath(context);
            //判断文件是否存在,如果存在,直接返回静态文件
            if (File.Exists(filePath))
            {
                PhysicalFileResult r = new PhysicalFileResult(filePath, "text/html; charset=utf-8");
                context.Result = r;
                return;
            }
            base.OnActionExecuting(context);
        }


        public override void OnActionExecuted(ActionExecutedContext context)
        {

            bool success = GetViewHtml(context, out var html);

            if (success)
            {
                // 压缩html
                if (IsHtmlCompression)
                    html = UglifyHtml(html);

                // 如果是测试,不用保存html页面
                if (!IsTest(context))
                {
                    string filePath = GetFilePath(context);
                    // 判断文件目录是否存在,如果目录不能存在,保存目录,这里存在一个多线程异步问题
                    string devicedir = System.IO.Path.GetDirectoryName(filePath);
                    lock (GlobalData.Locker)
                    {
                        if (!Directory.Exists(devicedir))
                        {
                            Directory.CreateDirectory(devicedir);
                        }
                    }

                    using (FileStream fs = File.Open(filePath, FileMode.Create))
                    {
                        using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8))
                        {
                            sw.Write(html);
                        }
                    }
                }
                //输出当前的结果
                ContentResult contentresult = new ContentResult();
                contentresult.Content = html;
                contentresult.ContentType = "text/html";
                context.Result = contentresult;
            }

            base.OnActionExecuted(context);

        }

        public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            return base.OnActionExecutionAsync(context, next);
        }

        /// <summary>
        /// 是否更新文件
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsUpdateOutputFile(FilterContext context)
        {
            return context.HttpContext.Request.Query.ContainsKey(UpdateFileQueryString);
        }
        /// <summary>
        /// 是否在测试
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsTest(FilterContext context)
        {
            return context.HttpContext.Request.Query.ContainsKey(TestQueryString);
        }

        /// <summary>
        /// 获得当前url对应静态页面文件应该存放的位置
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        string GetFilePath(FilterContext context)
        {
            string filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", OutputFolder + context.HttpContext.Request.Path.Value.Replace("/", "\\"));
            return filePath;
        }

        /// <summary>
        /// 获得视图响应的html
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        bool GetViewHtml(ActionExecutedContext context, out string html)
        {
            //获取结果
            IActionResult actionResult = context.Result;
            //判断结果是否是一个ViewResult
            if (actionResult is ViewResult)
            {
                ViewResult viewResult = actionResult as ViewResult;
                //下面的代码就是执行这个ViewResult,并把结果的html内容放到一个StringBuiler对象中
                var services = context.HttpContext.RequestServices;
                var viewEngine = services.GetService(typeof(ViewEngines.ICompositeViewEngine)) as ViewEngines.ICompositeViewEngine;
                ViewEngines.ViewEngineResult result = viewEngine.FindView(context, viewResult.ViewName, true);

                //var executor = services.GetRequiredService<ViewResultExecutor>();
                var option = services.GetRequiredService<IOptions<MvcViewOptions>>();
                //var result = executor.FindView(context, viewResult);
                result.EnsureSuccessful(originalLocations: null);
                var view = result.View;
                StringBuilder builder = new StringBuilder();
                using (var writer = new StringWriter(builder))
                {
                    var viewContext = new ViewContext(
                        context,
                        view,
                        viewResult.ViewData,
                        viewResult.TempData,
                        writer,
                        option.Value.HtmlHelperOptions);
                    view.RenderAsync(viewContext).GetAwaiter().GetResult();
                    //这句一定要调用,否则内容就会是空的
                    writer.Flush();
                }
                html = builder.ToString();
                return true;
            }
            html = "";
            return false;
        }

        /// <summary>
        /// 压缩HTML
        /// </summary>
        /// <param name="html"></param>
        /// <returns></returns>
        string UglifyHtml(string html)
        {
            var js = new NUglifyJsMinifier();
            var css = new NUglifyCssMinifier();

            XhtmlMinifier htmlMinifier = new XhtmlMinifier(null, css, js, null);
            var yasuo_result = htmlMinifier.Minify(html);
            if (yasuo_result.Errors.Count == 0)
            {
                return yasuo_result.MinifiedContent;
            }

            return html;
        }

    }


}

参考资料

参考一:ActionFilter中保存页面响应的html,下次请求直接返回

http://blog.baibaota.com/882.html

那我们就来具体的实现代码,首先我们定义一个过滤器类型,我们成为StaticFileHandlerFilterAttribute,这个类派生自框架中提供的ActionFilterAttribute,StaticFileHandlerFilterAttribute重写基类提供的两个方法:OnActionExecuted(动作执行后),OnActionExecuting(动作执行前),具体代码如下:

C# 全选
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class StaticFileHandlerFilterAttribute : ActionFilterAttribute
{
      public override void OnActionExecuted(ActionExecutedContext context){}
      public override void OnActionExecuting(ActionExecutingContext context){}
}

在OnActionExecuting中,需要判断下静态内容是否已经生成,如果已经生成直接输出内容,逻辑实现如下:

C# 全选
//按照一定的规则生成静态文件的名称,这里是按照area+"-"+controller+"-"+action+key规则生成
string controllerName = context.RouteData.Values["controller"].ToString().ToLower();
string actionName = context.RouteData.Values["action"].ToString().ToLower();
string area = context.RouteData.Values["area"].ToString().ToLower();
//这里的Key默认等于id,当然我们可以配置不同的Key名称
string id = context.RouteData.Values.ContainsKey(Key) ? context.RouteData.Values[Key].ToString() : "";
if (string.IsNullOrEmpty(id) && context.HttpContext.Request.Query.ContainsKey(Key))
{
    id = context.HttpContext.Request.Query[Key];
}
string filePath = Path.Combine(AppContext.BaseDirectory, "wwwroot", area, controllerName + "-" + actionName + (string.IsNullOrEmpty(id) ? "" : ("-" + id)) + ".html");
//判断文件是否存在
if (File.Exists(filePath))
{
  //如果存在,直接读取文件
   using (FileStream fs = File.Open(filePath, FileMode.Open))
   {
       using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
       {
        //通过contentresult返回文件内容
             ContentResult contentresult = new ContentResult();
             contentresult.Content = sr.ReadToEnd();
             contentresult.ContentType = "text/html";
             context.Result = contentresult;
        }
    }
}

在OnActionExecuted中我们需要结果动作结果,判断动作结果类型是否是一个ViewResult,如果是通过代码执行这个结果,获取结果输出,按照上面一样的规则,生成静态页,具体实现如下

C# 全选
//获取结果
IActionResult actionResult = context.Result;
 //判断结果是否是一个ViewResult
       if (actionResult is ViewResult)
       {
           ViewResult viewResult = actionResult as ViewResult;
           //下面的代码就是执行这个ViewResult,并把结果的html内容放到一个StringBuiler对象中
           var services = context.HttpContext.RequestServices;
           var executor = services.GetRequiredService<ViewResultExecutor>();
           var option = services.GetRequiredService<IOptions<MvcViewOptions>>();
           var result = executor.FindView(context, viewResult);
           result.EnsureSuccessful(originalLocations: null);
           var view = result.View;
           StringBuilder builder = new StringBuilder();
           using (var writer = new StringWriter(builder))
           {
               var viewContext = new ViewContext(
                   context,
                   view,
                   viewResult.ViewData,
                   viewResult.TempData,
                   writer,
                   option.Value.HtmlHelperOptions);
               view.RenderAsync(viewContext).GetAwaiter().GetResult();
               //这句一定要调用,否则内容就会是空的
               writer.Flush();
           }
           //按照规则生成静态文件名称
           string area = context.RouteData.Values["area"].ToString().ToLower();
           string controllerName = context.RouteData.Values["controller"].ToString().ToLower();
           string actionName = context.RouteData.Values["action"].ToString().ToLower();
           string id = context.RouteData.Values.ContainsKey(Key) ? context.RouteData.Values[Key].ToString() : "";
           if (string.IsNullOrEmpty(id) && context.HttpContext.Request.Query.ContainsKey(Key))
           {
               id = context.HttpContext.Request.Query[Key];
           }
           string devicedir = Path.Combine(AppContext.BaseDirectory, "wwwroot", area);
           if (!Directory.Exists(devicedir))
           {
               Directory.CreateDirectory(devicedir);
           }
           //写入文件
           string filePath = Path.Combine(AppContext.BaseDirectory, "wwwroot", area, controllerName + "-" + actionName + (string.IsNullOrEmpty(id) ? "" : ("-" + id)) + ".html");
           using (FileStream fs = File.Open(filePath, FileMode.Create))
           {
               using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8))
               {
                   sw.Write(builder.ToString());
               }
           }
           //输出当前的结果
           ContentResult contentresult = new ContentResult();
           contentresult.Content = builder.ToString();
           contentresult.ContentType = "text/html";
           context.Result = contentresult;
       }

上面提到的Key,我们直接增加对应的属性

C# 全选
public string Key
{
    get;set;
}

这样我们就可以使用这个过滤器了,使用的方法:在控制器或者控制器方法上增加 [StaticFileHandlerFilter]特性,如果想配置不同的Key,可以使用 [StaticFileHandlerFilter(Key="设置的值")]

C# 全选
//获取文件信息对象
FileInfo fileInfo=new FileInfo(filePath);
//结算时间间隔,如果小于等于两分钟,就直接输出,当然这里的规则可以改
TimeSpan ts = DateTime.Now - fileInfo.CreationTime;
if(ts.TotalMinutes<=2)
{
   using (FileStream fs = File.Open(filePath, FileMode.Open))
   {
       using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
       {
            ContentResult contentresult = new ContentResult();
            contentresult.Content = sr.ReadToEnd();
            contentresult.ContentType = "text/html";
            context.Result = contentresult;
       }
    }
}

如果var executor = services.GetRequiredService<ViewResultExecutor>();报错

No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor' has been registered.”

请查阅 参考二 

静态化已经实现了,我们还需要考虑更新的事,如果后台把一篇文章更新了,我们得把静态页也更新下,方案有很多:一种是在后台进行内容更新时,同步把对应的静态页删除即可。我们这里介绍另外一种,定时更新,就是让静态页有一定的有效期,过了这个有效期自动更新。要实现这个逻辑,我们需要在OnActionExecuting方法中获取静态页的创建时间,然后跟当前时间对比,判断是否已过期,如果未过期直接输出内容,如果已过期,继续执行后面的逻辑。具体代码如下:

生成了静态内容后,就剩下地址规则修改,这个只需要在Startup调用UseMvc时,规则中使用.html结尾的地址即可。

 

到此伪静态就实现好了。目前的处理方法,只能在一定程度上能够提高访问性能,但是针对大型的门户系统来说,可能远远不够。按照上面介绍的方式,可以再进行其他功能扩展,比如生成静态页后可以发布到CDN上,也可以发布到单独的一个内容服务器,等等。不管是什么方式,实现思路都是一样的。

参考二:ASP.NET Coire MVC ViewResult生成HTML字符串的正确方法

https://stackoverflow.com/questions/53295126/how-can-i-save-html-of-rendered-view

根据ViewResult生成html字符串

 

C#全选
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.IO;
using System.Threading.Tasks;

namespace CC.Web.Helpers
{
    public static class ControllerExtensions
    {
        public static async Task<string> RenderViewAsync<TModel>(this Controller controller, string viewName, TModel model, bool partial = false)
        {
            if (string.IsNullOrEmpty(viewName))
            {
                viewName = controller.ControllerContext.ActionDescriptor.ActionName;
            }

            controller.ViewData.Model = model;

            using (var writer = new StringWriter())
            {
                IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
                ViewEngineResult viewResult = viewEngine.FindView(controller.ControllerContext, viewName, !partial);

                if (viewResult.Success == false)
                {
                    return $"A view with the name {viewName} could not be found";
                }

                ViewContext viewContext = new ViewContext(
                    controller.ControllerContext,
                    viewResult.View,
                    controller.ViewData,
                    controller.TempData,
                    writer,
                    new HtmlHelperOptions()
                );

                await viewResult.View.RenderAsync(viewContext);

                return writer.GetStringBuilder().ToString();
            }
        }
    }
}

 

参考三:动态页面转静态页面

 

https://github.com/toolgood/StaticPage/blob/master/HtmlStaticFileAttribute.cs

C# 全选
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Microsoft.AspNetCore.Mvc
{
    /// <summary>
    /// 生成Html静态文件
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class HtmlStaticFileAttribute : ActionFilterAttribute, IPageFilter
    {
        private static ConcurrentDictionary<string, PageCache> MemoryCache = new ConcurrentDictionary<string, PageCache>();
        private struct PageCache
        {
            public DateTime LastWriteTimeUtc;
            public byte[] Html;
            public byte[] GzipHtml;
            public byte[] BrHtml;
            public PageCache(DateTime dt, byte[] html, byte[] gzip, byte[] br)
            {
                LastWriteTimeUtc = dt;
                Html = html;
                GzipHtml = gzip;
                BrHtml = br;
            }
        }

        /// <summary>
        /// 页面更新参数,用于更新页面,更新文件 如 https://localhost:44345/?__update__
        /// </summary>
        public static string UpdateFileQueryString = "__update__";
        /// <summary>
        /// 页面测试参数,测试页面,不更新文件 如  https://localhost:44345/?__test__
        /// </summary>
        public static string TestQueryString = "__test__";

        /// <summary>
        /// 开发模式
        /// </summary>
        public static bool IsDevelopmentMode = false;

        /// <summary>
        /// 支持Url参数,不推荐使用
        /// </summary>
        public static bool UseQueryString = false;
        /// <summary>
        /// 页面缓存时间 单位:分钟
        /// </summary>
        public static int ExpireMinutes = 1;

        /// <summary>
        /// 静态文件保存路径, 如果为空,则默认放在 {dll文件夹}\html 文件夹下
        /// </summary>
        public static string OutputFolder;
        /// <summary>
        /// 使用GZIP压缩,会另生成一个单独的文件,以空间换时间,火狐、IE11 对于http://开头的地址不支持 br 压缩
        /// </summary>
        public static bool UseGzipCompress = false;

        /// <summary>
        /// 使用BR压缩,会另生成一个单独的文件,以空间换时间
        /// </summary>
        public static bool UseBrCompress = false;

        /// <summary>
        /// 指定方法,保存前会进行最小化处理, 推荐使用 WebMarkupMin
        /// </summary>
        public static event Func<string, string> MiniFunc;

        /// <summary>
        /// 当前页面缓存时间,(单位:分钟),最小值:1
        /// </summary>
        private int? CurrPageExpireMinutes { get; set; }

        /// <summary>
        /// 使用内存缓存
        /// </summary>
        public bool UseMemoryCache { get; set; }

        /// <summary>
        /// 生成Html静态文件
        /// </summary>
        public HtmlStaticFileAttribute() { }

        /// <summary>
        /// 生成Html静态文件
        /// </summary>
        /// <param name="expireMinutes">当前页面缓存时间(单位:分钟),最小值:1</param>
        public HtmlStaticFileAttribute(int expireMinutes)
        {
            if (expireMinutes <= 0)
            {
                expireMinutes = 1;
            }
            CurrPageExpireMinutes = expireMinutes;
        }

        #region 用于 Pages
        public void OnPageHandlerExecuted(PageHandlerExecutedContext context) { }

        public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
        {
            if (IsDevelopmentMode == false && IsTest(context) == false && IsUpdateOutputFile(context) == false)
            {
                var result = GetStaticFileResult(context);
                if (result != null)
                {
                    context.Result = result;
                    return;
                }
            }
        }

        public void OnPageHandlerSelected(PageHandlerSelectedContext context) { }
        #endregion

        #region 用于 Views

        public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            if (IsDevelopmentMode == false && IsTest(context) == false && IsUpdateOutputFile(context) == false)
            {
                var result = GetStaticFileResult(context);
                if (result != null)
                {
                    context.Result = result;
                    return;
                }
            }
            await base.OnActionExecutionAsync(context, next);
        }

        #endregion

        /// <summary>
        /// 尝试获取静态文件
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private ActionResult GetStaticFileResult(FilterContext context)
        {
            var filePath = GetOutputFilePath(context);
            var response = context.HttpContext.Response;
            // 从内存获取文件
            if (UseMemoryCache)
            {
                if (MemoryCache.TryGetValue(filePath, out PageCache page))
                {
                    var etag = page.LastWriteTimeUtc.Ticks.ToString();
                    if (context.HttpContext.Request.Headers["If-None-Match"] == etag)
                    {
                        return new StatusCodeResult(304);
                    }
                    if (UseBrCompress || UseGzipCompress)
                    {
                        var sp = context.HttpContext.Request.Headers["Accept-Encoding"].ToString().ToLower().Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
                        if (UseBrCompress && sp.Contains("br") && File.Exists(filePath + ".br"))
                        {
                            response.Headers["Content-Encoding"] = "br";
                            return new FileContentResult(page.BrHtml, "text/html");
                        }
                        else if (UseGzipCompress && sp.Contains("gzip") && File.Exists(filePath + ".gzip"))
                        {
                            response.Headers["Content-Encoding"] = "gzip";
                            return new FileContentResult(page.GzipHtml, "text/html");
                        }
                    }
                    return new FileContentResult(page.Html, "text/html");
                }
            }
            if (File.Exists(filePath))
            {
                var fi = new FileInfo(filePath);
                var etag = fi.LastWriteTimeUtc.Ticks.ToString();
                if (context.HttpContext.Request.Headers["If-None-Match"] == etag)
                {
                    return new StatusCodeResult(304);
                }
                if (UseMemoryCache)
                {
                    var html = File.ReadAllText(filePath);
                    SaveHtmlResultToMemoryCache(filePath, html);
                }

                response.Headers["Cache-Control"] = "max-age=" + (CurrPageExpireMinutes ?? ExpireMinutes) * 60;
                response.Headers["Etag"] = etag;
                response.Headers["Date"] = DateTime.Now.ToString("r");
                response.Headers["Expires"] = DateTime.Now.AddMinutes(CurrPageExpireMinutes ?? ExpireMinutes).ToString("r");

                if (UseBrCompress || UseGzipCompress)
                {
                    var sp = context.HttpContext.Request.Headers["Accept-Encoding"].ToString().ToLower().Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
                    if (UseBrCompress && sp.Contains("br") && File.Exists(filePath + ".br"))
                    {
                        response.Headers["Content-Encoding"] = "br";
                        return new PhysicalFileResult(filePath + ".br", "text/html");
                    }
                    else if (UseGzipCompress && sp.Contains("gzip") && File.Exists(filePath + ".gzip"))
                    {
                        response.Headers["Content-Encoding"] = "gzip";
                        return new PhysicalFileResult(filePath + ".gzip", "text/html");
                    }
                }
                return new PhysicalFileResult(filePath, "text/html");
            }
            return null;
        }


        public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
        {
            // 开发模式,已处理的,测试,不用保存到本地目录
            if (IsDevelopmentMode || context.Result is StatusCodeResult || context.Result is FileResult || IsTest(context))
            {
                await base.OnResultExecutionAsync(context, next);
                return;
            }

            var filePath = GetOutputFilePath(context);
            var response = context.HttpContext.Response;
            if (!response.Body.CanRead || !response.Body.CanSeek)
            {
                using (var ms = new MemoryStream())
                {
                    var old = response.Body;
                    response.Body = ms;

                    await base.OnResultExecutionAsync(context, next);

                    if (response.StatusCode == 200)
                    {
                        await SaveHtmlResult(response.Body, filePath);
                    }
                    ms.Position = 0;
                    await ms.CopyToAsync(old);
                    response.Body = old;
                }
            }
            else
            {
                await base.OnResultExecutionAsync(context, next);
                var old = response.Body.Position;
                if (response.StatusCode == 200)
                {
                    await SaveHtmlResult(response.Body, filePath);
                }
                response.Body.Position = old;
            }
            //更新时,不添加页面缓存
            if (IsUpdateOutputFile(context) == false)
            {
                var fi = new FileInfo(filePath);
                var etag = fi.LastWriteTimeUtc.Ticks.ToString();
                context.HttpContext.Response.Headers["Cache-Control"] = "max-age=" + (CurrPageExpireMinutes ?? ExpireMinutes) * 60;
                context.HttpContext.Response.Headers["Etag"] = etag;
                context.HttpContext.Response.Headers["Date"] = DateTime.Now.ToString("r");
                context.HttpContext.Response.Headers["Expires"] = DateTime.Now.AddMinutes(CurrPageExpireMinutes ?? ExpireMinutes).ToString("r");
            }
        }


        /// <summary>
        /// 保存Html结束为静态文件
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="filePath"></param>
        /// <returns></returns>
        private async Task SaveHtmlResult(Stream stream, string filePath)
        {
            stream.Position = 0;
            var responseReader = new StreamReader(stream);
            var responseContent = await responseReader.ReadToEndAsync();
            if (MiniFunc != null)
            {//进行最小化处理
                responseContent = MiniFunc(responseContent);
            }
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));

            await File.WriteAllTextAsync(filePath, responseContent);


            if (UseGzipCompress || UseBrCompress || UseMemoryCache)
            {
                byte[] gzip = new byte[0];
                byte[] br = new byte[0];

                var htmlbs = Encoding.UTF8.GetBytes(responseContent);
                if (UseGzipCompress)
                {
                    gzip = GzipCompress(htmlbs, false);
                    await File.WriteAllBytesAsync(filePath + ".gzip", gzip);
                }
                if (UseBrCompress)
                {
                    br = BrCompress(htmlbs, false);
                    await File.WriteAllBytesAsync(filePath + ".br", br);
                }
                if (UseMemoryCache)
                {
                    var pg = new PageCache(new FileInfo(filePath).LastWriteTimeUtc, htmlbs, gzip, br);
                    MemoryCache[filePath] = pg;
                }
            }
        }

        /// <summary>
        /// 读取文件,保存到内存缓存内
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="html"></param>
        private void SaveHtmlResultToMemoryCache(string filePath, string html)
        {
            byte[] htmlbs = Encoding.UTF8.GetBytes(html);
            byte[] gzip = new byte[0];
            byte[] br = new byte[0];
            if (UseGzipCompress)
            {
                gzip = GzipCompress(htmlbs, false);
            }
            if (UseBrCompress)
            {
                br = BrCompress(htmlbs, false);
            }
            var pg = new PageCache(new FileInfo(filePath).LastWriteTimeUtc, htmlbs, gzip, br);
            MemoryCache[filePath] = pg;
        }

        /// <summary>
        /// 是否在测试
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsTest(FilterContext context)
        {
            return context.HttpContext.Request.Query.ContainsKey(TestQueryString);
        }
        /// <summary>
        /// 是否更新文件
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private bool IsUpdateOutputFile(FilterContext context)
        {
            return context.HttpContext.Request.Query.ContainsKey(UpdateFileQueryString);
        }
        /// <summary>
        /// 获取更新文件路径
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        private string GetOutputFilePath(FilterContext context)
        {
            string dir = OutputFolder;
            if (string.IsNullOrEmpty(dir))
            {
                dir = Path.Combine(Path.GetDirectoryName(typeof(HtmlStaticFileAttribute).Assembly.Location), "html");
                OutputFolder = dir;
            }

            var t = context.HttpContext.Request.Path.ToString().Replace("//", Path.DirectorySeparatorChar.ToString()).Replace("/", Path.DirectorySeparatorChar.ToString());
            if (t.EndsWith(Path.DirectorySeparatorChar))
            {
                t += "index";
            }
            if (UseQueryString)
            {
                var list = new HashSet<string>();
                foreach (var item in context.HttpContext.Request.Query.Keys)
                {
                    if (item != UpdateFileQueryString)
                    {
                        var value = context.HttpContext.Request.Query[item];
                        if (string.IsNullOrEmpty(value))
                        {
                            list.Add($"{list}_");
                        }
                        else
                        {
                            list.Add($"{list}_{value}");
                        }
                    }
                }
                t += Regex.Replace(string.Join(",", list), "[^0-9_a-zA-Z\u4E00-\u9FCB\u3400-\u4DB5\u3007]", "_");
            }

            t = t.TrimStart(Path.DirectorySeparatorChar);
            return Path.Combine(dir, t) + ".html";
        }

        /// <summary>
        /// Gzip压缩
        /// </summary>
        /// <param name="data">要压缩的字节数组</param>
        /// <param name="fastest">快速模式</param>
        /// <returns>压缩后的数组</returns>
        private byte[] GzipCompress(byte[] data, bool fastest = false)
        {
            if (data == null || data.Length == 0)
                return data;
            try
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    var level = fastest ? CompressionLevel.Fastest : CompressionLevel.Optimal;
                    using (GZipStream zStream = new GZipStream(stream, level))
                    {
                        zStream.Write(data, 0, data.Length);
                    }
                    return stream.ToArray();
                }
            }
            catch
            {
                return data;
            }
        }

        /// <summary>
        /// Br压缩
        /// </summary>
        /// <param name="data">要压缩的字节数组</param>
        /// <param name="fastest">快速模式</param>
        /// <returns>压缩后的数组</returns>
        private byte[] BrCompress(byte[] data, bool fastest = false)
        {
            if (data == null || data.Length == 0)
                return data;
            try
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    var level = fastest ? CompressionLevel.Fastest : CompressionLevel.Optimal;
                    using (BrotliStream zStream = new BrotliStream(stream, level))
                    {
                        zStream.Write(data, 0, data.Length);
                    }
                    return stream.ToArray();
                }
            }
            catch
            {
                return data;
            }
        }


    }
}

一个文件搞定动态页面转静态页面,支持.NET CORE 3.1 ,支持MVC和PageModel。

One file gets dynamic pages to static pages, supports .NET CORE 3.1, and supports MVC and PageModel.

快速入门 MVC(Quick Start MVC)

1、将HtmlStaticFileAttribute.cs放到项目下;

Put HtmlStaticFileAttribute.cs under the project;

2、在控制器文件中,添加命名空间Microsoft.AspNetCore.Mvc;

In the controller file, add the namespace Microsoft.AspNetCore.Mvc;

3、在类名或Action方法上添加[HtmlStaticFile]。

Add [HtmlStaticFile] to the class name or Action method.

相关如下代码(Related code):

C# 全选
using Microsoft.AspNetCore.Mvc;

namespace StaticPage.Mvc.Controllers
{
    public class HomeController : Controller
    {
    
        [HtmlStaticFile]
        [HttpGet("/Count")]
        public IActionResult Count()
        {
            c++;
            ViewBag.C = c;
            return View();
        }

	}
}

快速入门 PageModel(Quick Start PageModel)

1、将HtmlStaticFileAttribute.cs放到项目下;

Put HtmlStaticFileAttribute.cs under the project;

2、在PageModel文件中,添加命名空间Microsoft.AspNetCore.Mvc。

In the PageModel file, add the namespace Microsoft.AspNetCore.Mvc.

3、在类名上添加[HtmlStaticFile],注:方法上添加[HtmlStaticFile]是无效的。

Add [HtmlStaticFile] to the class name. Note: Adding [[HtmlStaticFile] `to the method is invalid.

相关如下代码(Related code):

C# 全选
using Microsoft.AspNetCore.Mvc;

namespace StaticPage.Pages
{
    [HtmlStaticFile]
    public class CountModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}

设置缓存文件夹 (Set cache folder)

C# 全选
HtmlStaticFileAttribute.OutputFolder = @"D:\html";

使用压缩 (Use compression)

C# 全选
HtmlStaticFileAttribute.UseBrCompress = true;
HtmlStaticFileAttribute.UseGzipCompress = true;

设置页面缓存时间 (Set page cache time)

C# 全选
HtmlStaticFileAttribute.ExpireMinutes = 3;

使用开发模式 (Use development mode)

在开发模式,页面不会被缓存,便于开发调试。

In the development mode, the page will not be cached, which is convenient for development and debugging.

C# 全选
HtmlStaticFileAttribute.IsDevelopmentMode = true;

支持Url参数,不推荐使用 (Support Url parameter, not recommended)

C# 全选
HtmlStaticFileAttribute.UseQueryString = true;

使用Html压缩 (Use Html compression)

推荐使用WebMarkupMin来压缩Html。

It is recommended to use WebMarkupMin to compress Html.

C# 全选
HtmlStaticFileAttribute.MiniFunc += (string html) => {
                var js = new NUglifyJsMinifier();
                var css = new NUglifyCssMinifier();

                XhtmlMinifier htmlMinifier = new XhtmlMinifier(null, css, js, null);
                var result = htmlMinifier.Minify(html);
                if (result.Errors.Count == 0) {
                    return result.MinifiedContent;
                }
                return html;
            };

更新文件缓存 (Update file cache)

在Url地址后面添加参数“update”,访问一下就可以生成新的静态页面。

Add the parameter "update" after the Url address, and you can generate a new static page after visiting it.

Markup 全选
https://localhost:44304/Count?__update__

测试页面,不更新文件缓存 (Test page without updating file cache)

在Url地址后面添加参数“test”,访问一下就可以看到最新页面。

Add the parameter "test" after the Url address, you can see the latest page after visiting.

Markup 全选
https://localhost:44304/Count?__test__

 

 

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:中国国家产品分类目录下载
下一篇:SQLite数据库删除数据后数据库文件大小不变
评论列表

发表评论

评论内容
昵称:
关联文章

ASP.NET Core MVC 过滤器ActionFilter保存页面生成html静态页面文件
asp.net - ASP.NET Core MVC 嵌套 TagHelper
asp.net core MVC路由添加.html静态url时报错
ASP.NET Core MVC路由约束
ASP.NET MVCASP.NET Core MVC获取当前URL/Controller/Action
.net core MVC页面源码文件文被编码
ASP.NET Core web API使用Swagger/OpenAPI(Swashbuckle)
【推荐】Razor文件编译 ASP.NET Core
ASP.NET MVC快速入门(一)
.NET Core生成后没有Nugetdll文件
.NET Core发布后IIS部署无法访问静态文件
C#基础系列-过滤器与特性
ASP.NET Core调用另一个控制器并生成返回视图html
ASP.NET+MVC入门踩坑笔记 (一) 创建项目 项目配置运行 以及简单Api搭建
VS调试运行ASP.NET MVC项目,上传静态资源图片404问题,Debug路径
ASP.NET Core官网教程,资料查找
asp.net core mvc修改cshtml试图热加载动态更新
C# asp.net mvc 创建虚拟目录
C# ASP.NET Core开发学生信息管理系统(一)
Quartz.NET使用

联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码