【问题标题】:MVC4 StyleBundle: Can you add a cache-busting query string in Debug mode?MVC4 StyleBundle:您可以在调试模式下添加缓存清除查询字符串吗?
【发布时间】:2016-05-10 00:02:45
【问题描述】:

我有一个 MVC 应用程序,我正在使用 StyleBundle 类来渲染 CSS 文件,如下所示:

bundles.Add(new StyleBundle("~/bundles/css").Include("~/Content/*.css"));

我遇到的问题是,在Debug 模式下,CSS url 是单独呈现出来的,并且我有一个积极缓存这些 url 的网络代理。在Release 模式下,我知道在最终 url 中添加了一个查询字符串,以使每个版本的任何缓存都无效。

是否可以将StyleBundle 配置为在Debug 模式下添加随机查询字符串以产生以下输出来解决缓存问题?

<link href="/stylesheet.css?random=some_random_string" rel="stylesheet"/>

【问题讨论】:

    标签: caching asp.net-mvc-4 bundle


    【解决方案1】:

    您只需要一个唯一的字符串。它不一定是哈希。我们使用文件的 LastModified 日期并从那里获取 Ticks。正如@Todd 指出的那样,打开和读取文件的成本很高。 Ticks 足以输出一个在文件更改时更改的唯一编号。

    internal static class BundleExtensions
    {
        public static Bundle WithLastModifiedToken(this Bundle sb)
        {
            sb.Transforms.Add(new LastModifiedBundleTransform());
            return sb;
        }
        public class LastModifiedBundleTransform : IBundleTransform
        {
            public void Process(BundleContext context, BundleResponse response)
            {
                foreach (var file in response.Files)
                {
                    var lastWrite = File.GetLastWriteTime(HostingEnvironment.MapPath(file.IncludedVirtualPath)).Ticks.ToString();
                    file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", lastWrite);
                }
            }
        }
    }
    

    以及如何使用它:

    bundles.Add(new StyleBundle("~/bundles/css")
        .Include("~/Content/*.css")
        .WithLastModifiedToken());
    

    这就是 MVC 写的:

    <link href="bundles/css/site.css?v=635983900813469054" rel="stylesheet"/>
    

    也适用于脚本包。

    【讨论】:

    • 这太棒了!并且可以很容易地适应使用软件版本号而不是日期。谢谢!
    • 如此简单。非常感谢!
    • @H Dog 无法为我工作 cshtml 文件无法解析包
    【解决方案2】:

    您可以创建一个自定义 IBundleTransform 类来执行此操作。这是一个示例,它将使用文件内容的哈希附加一个 v=[filehash] 参数。

    public class FileHashVersionBundleTransform: IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {
            foreach(var file in response.Files)
            {
                using(FileStream fs = File.OpenRead(HostingEnvironment.MapPath(file.IncludedVirtualPath)))
                {
                    //get hash of file contents
                    byte[] fileHash = new SHA256Managed().ComputeHash(fs);
    
                    //encode file hash as a query string param
                    string version = HttpServerUtility.UrlTokenEncode(fileHash);
                    file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", version);
                }                
            }
        }
    }
    

    然后,您可以通过将类添加到捆绑包的 Transforms 集合来注册该类。

    new StyleBundle("...").Transforms.Add(new FileHashVersionBundleTransform());
    

    现在版本号只有在文件内容改变时才会改变。

    【讨论】:

    • 为什么不直接使用文件 LastWrite 日期。然后,您不必为 SHA256 执行所有磁盘读取和 CPU 操作。如果你坚持散列,MD5 就足够了——你不是在追求安全性,你是在追求一个独特的散列(足够好),CPU 周期低。
    • 不,这不起作用,您的示例对我不起作用- response.Files 是 FileInfo 对象的 Enumarable。 (Web.Optimization 版本=1.0.0.0)。看来您需要使用 1.1+ 版
    • 感谢@bingles,为了版本控制,我稍微修改了代码,以便使用主 DLL 程序集版本,每次自动部署后都会自动递增。
    • 你必须为每个包创建一个新的 FileHashVersionBundleTransform 还是可以重复使用一个?
    • @Marie 据我所知,您应该能够重用单个实例。
    【解决方案3】:

    这个库可以在调试模式下将缓存清除哈希添加到您的捆绑文件中,以及其他一些缓存清除内容:https://github.com/kemmis/System.Web.Optimization.HashCache

    您可以将 HashCache 应用于 BundlesCollection 中的所有包

    在 BundlesCollection 实例上执行 ApplyHashCache() 扩展方法 在所有捆绑包都添加到集合之后

    BundleTable.Bundles.ApplyHashCache();
    

    或者您可以将 HashCache 应用于单个 Bundle

    创建 HashCacheTransform 的实例并将其添加到您想要的捆绑实例中 将 HashCache 应用到。

    var myBundle = new ScriptBundle("~/bundle_virtual_path").Include("~/scripts/jsfile.js");
    myBundle.Transforms.Add(new HashCacheTransform());
    

    【讨论】:

    • 我试过这个,但它需要将我们的 WebGrease 版本从 1.1.0 更新到 1.5.2,这引入了另一个错误,所以我回滚了它。此软件包的初始修订版(版本 1.0.0)不需要我们更新 WebGrease,但是我决定不安装该版本,而是使用 @bingles 接受的答案,因为它给了我们完全的控制权。
    • 除了现在我看到接受的答案还需要 WebGrease 1.5.2。
    【解决方案4】:

    我遇到了同样的问题,但升级后客户端浏览器中的缓存版本。我的解决方案是在我自己的渲染器中包装对@Styles.Render("~/Content/css") 的调用,将我们的版本号附加到查询字符串中,如下所示:

        public static IHtmlString RenderCacheSafe(string path)
        {
            var html = Styles.Render(path);
            var version = VersionHelper.GetVersion();
            var stringContent = html.ToString();
    
            // The version should be inserted just before the closing quotation mark of the href attribute.
            var versionedHtml = stringContent.Replace("\" rel=", string.Format("?v={0}\" rel=", version));
            return new HtmlString(versionedHtml);
        }
    

    然后在视图中我是这样的:

    @RenderHelpers.RenderCacheSafe("~/Content/css")
    

    【讨论】:

    • 如果您卡在 Web.Optimization 版本 1.0.0.0 上,这是一个很好的解决方法
    【解决方案5】:

    目前还没有,但预计很快就会添加(目前计划在 1.1 稳定版本中发布,您可以在此处跟踪此问题:Codeplex

    【讨论】:

      【解决方案6】:

      请注意,这是为脚本编写的,但也适用于样式(只需更改这些关键词)

      基于@Johan 的回答:

      public static IHtmlString RenderBundle(this HtmlHelper htmlHelper, string path)
      {
          var context = new BundleContext(htmlHelper.ViewContext.HttpContext, BundleTable.Bundles, string.Empty);
          var bundle = System.Web.Optimization.BundleTable.Bundles.GetBundleFor(path);
          var html = System.Web.Optimization.Scripts.Render(path).ToString();
          foreach (var item in bundle.EnumerateFiles(context))
          {
              if (!html.Contains(item.Name))
                  continue;
      
              html = html.Replace(item.Name, item.Name + "?" + item.LastWriteTimeUtc.ToString("yyyyMMddHHmmss"));
          }
      
          return new HtmlString(html);
      }
      
      public static IHtmlString RenderStylesBundle(this HtmlHelper htmlHelper, string path)
      {
          var context = new BundleContext(htmlHelper.ViewContext.HttpContext, BundleTable.Bundles, string.Empty);
          var bundle = System.Web.Optimization.BundleTable.Bundles.GetBundleFor(path);
          var html = System.Web.Optimization.Styles.Render(path).ToString();
          foreach (var item in bundle.EnumerateFiles(context))
          {
              if (!html.Contains(item.Name))
                  continue;
      
              html = html.Replace(item.Name, item.Name + "?" + item.LastWriteTimeUtc.ToString("yyyyMMddHHmmss"));
          }
      
          return new HtmlString(html);
      }
      

      用法:

      @Html.RenderBundle("...")
      @Html.RenderStylesBundle("...")
      

      更换

      @Scripts.Render("...")
      @Styles.Render("...")
      

      好处:

      • 适用于 System.Web.Optimizations v1.0.0.0
      • 适用于捆绑包中的多个文件
      • 获取每个文件而不是组的文件修改日期,而不是散列

      此外,当您需要快速解决 Bundler 问题时:

      public static MvcHtmlString ResolveUrl(this HtmlHelper htmlHelper, string url)
      {
          var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
          var resolvedUrl = urlHelper.Content(url);
      
          if (resolvedUrl.ToLower().EndsWith(".js") || resolvedUrl.ToLower().EndsWith(".css"))
          {
              var localPath = HostingEnvironment.MapPath(resolvedUrl);
              var fileInfo = new FileInfo(localPath);
              resolvedUrl += "?" + fileInfo.LastWriteTimeUtc.ToString("yyyyMMddHHmmss");
          }
      
          return MvcHtmlString.Create(resolvedUrl);
      }
      

      用法:

      <script type="text/javascript" src="@Html.ResolveUrl("~/Scripts/jquery-1.9.1.min.js")"></script>
      

      替换:

      <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")"></script>
      

      (也替换了许多其他替代查找)

      【讨论】:

        猜你喜欢
        • 2014-02-06
        • 1970-01-01
        • 2011-08-24
        • 2014-12-17
        • 2023-03-28
        • 1970-01-01
        • 2015-06-06
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多