ASP.NET Core MVC 在过滤器ActionFilter中保存页面的生成的html静态页面文件
在大型网站系统中,为了提高系统访问性能,往往会把一些不经常变得内容发布成静态页,比如商城的产品详情页,新闻详情页,这些信息一旦发布后,变化的频率不会很高,如果还采用动态输出的方式进行处理的话,肯定会给服务器造成很大的资源浪费。但是我们又不能针对这些内容都独立制作静态页,所以我们可以在系统中利用伪静态的方式进行处理,至于什么是伪静态,大家可以百度下。我们这里就来介绍一下,在asp.net core mvc中实现伪静态的方式。
mvc框架中,view代表的是视图,它执行的结果就是最终输出到客户端浏览器的内容,包含html,css,js等。如果我们想实现静态化,我们就需要把view执行的结果保存成一个静态文件,保存到指定的位置上,比如磁盘、分布式缓存等,下次再访问就可以直接读取保存的内容,而不用再执行一次业务逻辑。那asp.net core mvc要实现这样的功能,应该怎么做?答案是使用过滤器,在mvc框架中,提供了多种过滤器类型,这里我们要使用的是动作过滤器,动作过滤器提供了两个时间点:动作执行前,动作执行后。我们可以在动作执行前,先判断是否已经生成了静态页,如果已经生成,直接读取文件内容输出即可,后续的逻辑就执行跳过。如果没有生产,就继续往下走,在动作执行后这个阶段捕获结果,然后把结果生成的静态内容进行保存。
我的ActionFilter
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(动作执行前),具体代码如下:
[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中,需要判断下静态内容是否已经生成,如果已经生成直接输出内容,逻辑实现如下:
//按照一定的规则生成静态文件的名称,这里是按照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,如果是通过代码执行这个结果,获取结果输出,按照上面一样的规则,生成静态页,具体实现如下
//获取结果
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,我们直接增加对应的属性
public string Key
{
get;set;
}
这样我们就可以使用这个过滤器了,使用的方法:在控制器或者控制器方法上增加 [StaticFileHandlerFilter]特性,如果想配置不同的Key,可以使用 [StaticFileHandlerFilter(Key="设置的值")]
//获取文件信息对象
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字符串
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
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):
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):
using Microsoft.AspNetCore.Mvc;
namespace StaticPage.Pages
{
[HtmlStaticFile]
public class CountModel : PageModel
{
public void OnGet()
{
}
}
}
设置缓存文件夹 (Set cache folder)
HtmlStaticFileAttribute.OutputFolder = @"D:\html";
使用压缩 (Use compression)
HtmlStaticFileAttribute.UseBrCompress = true;
HtmlStaticFileAttribute.UseGzipCompress = true;
设置页面缓存时间 (Set page cache time)
HtmlStaticFileAttribute.ExpireMinutes = 3;
使用开发模式 (Use development mode)
在开发模式,页面不会被缓存,便于开发调试。
In the development mode, the page will not be cached, which is convenient for development and debugging.
HtmlStaticFileAttribute.IsDevelopmentMode = true;
支持Url参数,不推荐使用 (Support Url parameter, not recommended)
HtmlStaticFileAttribute.UseQueryString = true;
使用Html压缩 (Use Html compression)
推荐使用WebMarkupMin来压缩Html。
It is recommended to use WebMarkupMin to compress Html.
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.
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.
https://localhost:44304/Count?__test__