ASP.NET MVC 实现页面静态化

作者:Dreamer
出处:http://www.dreamerlzy.com/blog/article/detail/asp.net-mvc-staticize
说明:本文版权归作者所有,欢迎转载,但未经作者同意时,请在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。
参考: ASP.NET MVC 页面静态化操作的思路   ASP.NET MVC 使用视图引擎实现页面静态化   利用ResultFilter实现asp.net mvc3 页面静态化   关于ASP.NET MVC生成纯静态后如何不再走路由直接访问静态页面   asp.net mvc3的静态化实现   

静态化的几种思路

  1. 使用html模板搭配自定义占位符标识 如vtemplate等

    有额外的学习成本

  2. 程序代码中拼接html生成

    此方式代码繁琐,后期维护困难,且前后端耦合严重。

  3. 模拟访问自身站点后获取源码

    需要准备2个站点 一个用于访问的原网站,一套供用户访问的静态网站 

    但此方式有较严重的性能问题 原理与爬虫抓取网站类似

  4. 通过asp.net webform或asp.net mvc本身的绑定,在输出流阶段时获取源码另存为html文件

    较完美,但此方案mvc中博主没有实践成功,存在上下文(context)的问题

    webform中可使用Server.Execute实现 

  5. 在用户访问时首先会检测站点是否存在对应的静态文件存在则直接输出静态文件内容,否则程序往下至输出流阶段,在此时存储流为静态页面

    较完美,且生成文件的操作只有在用户首次访问时才会进行(页面过期等操作可通过代码进行更细致的控制),但存在所有请求仍然会走IIS的问题。


本文介绍的是方案5的实践 站点方式为asp.net mvc

1、首先在Global.asax中的Application_BeginRequest方法时检测html文件是否已存在

protected void Application_BeginRequest(object sender, EventArgs e)
{
  StaticContentRewrite();
}

//检测html文件是否已存在 存在则直接输出 否则程序往下进行
private void StaticContentRewrite()
{
  LogMethod.WriteLog("Debug", "StaticContentRewrite");
  string nowpath = Context.Request.FilePath.ToLower();
  if (nowpath == "/" || nowpath.StartsWith("/index.html", StringComparison.OrdinalIgnoreCase))//首页 
  {
      if (File.Exists(Server.MapPath("index.html")))
      {
          Context.RewritePath("index.html");
      }
      else
      {
          Context.RewritePath("/index/index.html");
      }
  }
  else
  {
      nowpath = nowpath.LastIndexOf("/") == nowpath.Length - 1 ? nowpath.Substring(0, nowpath.Length - 2) : nowpath;
      LogMethod.WriteLog("Debug", "StaticContentRewriteReadHtml" + nowpath);
      string temp = "/html" + nowpath;
      if (File.Exists(Server.MapPath(temp)))
      {
          Context.RewritePath(temp);
      }
  }
}

2、实现特性类

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class GenerateStaticFileAttribute : ActionFilterAttribute
{
    #region 公共属性

    /// <summary>
    /// 过期时间,以小时为单位
    /// </summary>
    public int Expiration { get; set; }

    /// <summary>
    /// 文件后缀名
    /// </summary>
    public string Suffix { get; set; }

    /// <summary>
    /// 缓存目录
    /// </summary>
    public string CacheDirectory { get; set; }

    /// <summary>
    /// 指定生成的文件名
    /// </summary>
    public string FileName { get; set; }

    #endregion

    #region 构造函数

    /// <summary>
    /// 默认构造函数
    /// </summary>
    public GenerateStaticFileAttribute()
    {
        Expiration = 720;
        //Suffix = "html";
        CacheDirectory = AppDomain.CurrentDomain.BaseDirectory;
    }

    #endregion

    #region 方法

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        if (filterContext.RequestContext.HttpContext.Request.Url.ToString().Contains(".html") == false)
        {
            return;
        }
        var fileInfo = GetCacheFileInfo(filterContext);

        if ((fileInfo.Exists && fileInfo.CreationTime.AddHours(Expiration) < DateTime.Now) || !fileInfo.Exists)
        {
            var deleted = false;

            try
            {
                if (fileInfo.Exists)
                {
                    fileInfo.Delete();
                }

                deleted = true;
            }
            catch (Exception ex)
            {
                LogMethod.WriteLog("错误", string.Format("删除文件:{0}发生异常:{1}", fileInfo.FullName, ex.StackTrace));
            }

            var created = false;

            try
            {
                if (!fileInfo.Directory.Exists)
                {
                    fileInfo.Directory.Create();
                }

                created = true;
            }
            catch (IOException ex)
            {
                LogMethod.WriteLog("错误", string.Format("创建目录:{0}发生异常:{1}", fileInfo.DirectoryName, ex.StackTrace));
            }

            if (deleted && created)
            {
                FileStream fileStream = null;
                StreamWriter streamWriter = null;

                try
                {
                    var viewResult = filterContext.Result as ViewResult;
                    if (viewResult != null)
                    {
                        fileStream = new FileStream(fileInfo.FullName, FileMode.CreateNew, FileAccess.Write, FileShare.None);

                        streamWriter = new StreamWriter(fileStream);
                        var viewContext = new ViewContext(filterContext.Controller.ControllerContext, viewResult.View, viewResult.ViewData, viewResult.TempData, streamWriter);
                        viewResult.View.Render(viewContext, streamWriter);
                    }

                }
                catch (Exception ex)
                {
                    LogMethod.WriteLog("错误", string.Format("生成缓存文件:{0}发生异常:{1}", fileInfo.FullName, ex.StackTrace));
                }
                finally
                {
                    if (streamWriter != null)
                    {
                        streamWriter.Close();
                    }

                    if (fileStream != null)
                    {
                        fileStream.Close();
                    }
                }
            }
        }
    }

    /// <summary>
    /// 生成文件Key
    /// </summary>
    /// <param name="controllerContext">ControllerContext</param>
    /// <returns>文件Key</returns>
    protected virtual string GenerateKey(ControllerContext controllerContext)
    {
        var url = controllerContext.HttpContext.Request.Url.ToString();

        if (string.IsNullOrWhiteSpace(url))
        {
            return null;
        }
        string key = url.GetHtmlUrlByCA();


        //var th = new TigerHash();
        //var data = th.ComputeHash(Encoding.Unicode.GetBytes(url));
        //var key = Convert.ToBase64String(data, Base64FormattingOptions.None);
        //var key = Guid.NewGuid().ToString();
        //key = HttpUtility.UrlEncode(key);

        return key;
    }

    /// <summary>
    /// 获取静态的文件信息
    /// </summary>
    /// <param name="controllerContext">ControllerContext</param>
    /// <returns>缓存文件信息</returns>
    protected virtual FileInfo GetCacheFileInfo(ControllerContext controllerContext)
    {
        var fileName = string.Empty;

        if (string.IsNullOrWhiteSpace(FileName))
        {
            var key = GenerateKey(controllerContext);

            if (!string.IsNullOrWhiteSpace(key))
            {
                fileName = Path.Combine(CacheDirectory, string.IsNullOrWhiteSpace(Suffix) ? key : string.Format("{0}.{1}", key, Suffix));
            }
        }
        else
        {
            fileName = Path.Combine(CacheDirectory, FileName);
        }

        return new FileInfo(fileName);
    }
}

3、在需要静态化的地方注册特性 可对整个controller或单个action注册

说明:后缀可任意修改,页面链接的控制可通过扩展urlhelper类实现