首先解释一下什么是动态处理页面静态化
对于需要静态化的页面,第一次访问某个Action时,会先执行Action,并在页面渲染后向Response和服务器中网站的目录下都写入需要返回的html,而第二次访问此页时,在执行Action前,程序会先在指定目录下寻找是否存在当前请求对应的静态页面,如果有,则直接返回静态页面,如果没有,则按第一次访问此请求进行处理,即执行Action,并向Response和服务器中网站的目录下都写入需要返回的html。利用这种方式,可以在网站在请求的过程中,会动态的生成静态页面,而无需人工干预,方便快捷。
接下来直接上代码
/// <summary> /// 页面静态化过滤器 /// 思路:在执行Action前,先判断此Action返回的View()的静态文件是否存在 /// 如果存在,则直接返回静态文件。 /// 如果不存在,则利用OnResultExecuting,替换Response中的输出流,让渲染后的html写入到 /// 本过滤器定义的StringBuilder中,然后在OnResultExecuted中(页面渲染后),从StringBuilder /// 中获取html,并同时写入到静态文件和Response的内置输出流中 /// /// 注:由于我没找到直接从Response中获取输出流的html的方法,因此我这里是替换了Response中的output属性, /// 替换后,渲染后的结果就会输出到我们自己写的StringBuilder中(此时Response中并没有html),此时在渲染后, /// 再把StringBuilder中的html,分别写入静态文件和Response中(tw.write方法),完成此次请求。 /// </summary> public class StaticHtmlFilter : ActionFilterAttribute { //用于保存渲染后的html文本 static StringBuilder sb; //这几个Writer照着写就行了 static StringWriter sw; static HtmlTextWriter hw; static TextWriter tw; //自定义的静态页面的后缀名 static string ext = ".html"; //静态页面的绝对路径(包括后缀名) string fileName = null; ///静态页面的绝对路径(不包括后缀名) static string path = null; //静态文件是否存在 bool FileExists = false; /// <summary> /// Action执行前,判断当前页面是否已经被静态化(Views路径下是否存在html文件) /// 如果存在静态文件则直接设置filterContext的result,即返回html作为结果,而不执行Action中代码 /// 如果不存在静态页面文件,则不设置filterContext的result,此时将会执行Action中的代码 /// </summary> /// <param name="filterContext"></param> public override void OnActionExecuting(ActionExecutingContext filterContext) { //根据controller和action信息 string controller = filterContext.RouteData.Values["controller"].ToString(); string action = filterContext.RouteData.Values["action"].ToString(); object id=null; //路由中是否包含可选参数id,如果有,则在文件名也要体现 if (!filterContext.RouteData.Values.TryGetValue("id", out id)) { path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Views", controller, action); fileName = string.Format("{0}{1}", path, ext); } else { path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Views", controller, action); fileName = string.Format("{0}{1}{2}", path, id.ToString(),ext); } //拼装后缀名 FileExists = File.Exists(fileName); //如果文件存在,直接返回结果 if (FileExists) { filterContext.Result = new FileContentResult(File.ReadAllBytes(fileName), "text/html; charset=utf-8"); } } /// <summary> /// 执行完Action后,但渲染页面前执行此处 /// 渲染页面的意思是将cshtml中的后台代码,翻译为前台代码 /// /// </summary> /// <param name="filterContext"></param> public override void OnResultExecuting(ResultExecutingContext filterContext) {
if (!FileExists) { //保存html sb = new StringBuilder(); //两个writer sw = new StringWriter(sb); hw = new HtmlTextWriter(sw); //记住Response中原本输出流,用于返回本次请求的html,与下一句配合使用 //在渲染结束后,向tw内写入html内容 tw = filterContext.RequestContext.HttpContext.Response.Output; //过滤器自己输出流,用于获取渲染后的html内容 filterContext.RequestContext.HttpContext.Response.Output = hw; } } public override void OnResultExecuted(ResultExecutedContext filterContext) { //如果是静态文件不存在 if (!FileExists) { //获取渲染后的html文本 string res = sb.ToString(); //将文本写入到静态文件中 new Action(() => File.WriteAllText(fileName, res)).BeginInvoke(null, null); //向Response的输出流中写入本次请求的html tw.Write(sb.ToString()); } } }
什么样的Action适合静态化?
我认为有两种Action需要使用静态化
1.登录页面等无需向Action中传入参数而直接返回View的Action需要静态处理。
/// <summary> /// 登录 /// </summary> /// <returns></returns> [AllowAnonymous] [StaticHtmlFilter] public ActionResult Login() { var model = new LoginDto { ReturnUrl = Request.QueryString["ReturnUrl"], LoginName = "admin", Password = "qwaszx" }; if (User.Identity.IsAuthenticated) { if (model.ReturnUrl.IsNotBlank()) return Redirect(model.ReturnUrl); return RedirectToAction("Index"); } return View(model); }
2.通过一个参数进行查询的Action(注意是查询,非编辑)
/// <summary> /// 编辑 /// </summary> /// <param name="id"></param> /// <returns></returns> [StaticHtmlFilter] public async Task<ActionResult> Edit(string id) { var model = await _menuService.Find(id); return View(model); }
注意事项
对于经常需要编辑的内容的查询页面,如商品列表如使用动态处理静态化页面,则应在编辑商品信息后,删除服务器指定目录下的静态页面,以便于页面更新。
当然我们也可以在上面的过滤器中的 OnActionExecuting 方法在判断文件是否存在时,先判断本次请求是否是编辑操作,如果是则删除相应的静态文件重新生成即可。
注:有些地方我想得还不够充分,希望大家多提意见。