如何在MVC4中强制BundleCollection清除缓存的脚本包?

87

...或者说我是怎样放下顾虑,开始使用微软完全没有文档记录的API写代码。是否有关于官方System.Web.Optimization发布的实际文档呢?因为我真的找不到任何文档,没有XML文档,并且所有博客文章都涉及到与RC API截然不同的内容。总之...

我正在编写一些代码来自动解析javascript依赖项,并根据这些依赖关系即时创建bundle。一切都很好,除非您在不重新启动应用程序的情况下编辑脚本或进行其他更改会影响bundle,否则更改不会反映出来。因此,我添加了一个选项来禁用开发中的依赖项缓存。

但是,显然,BundleTables即使bundle集合已更改,也会缓存URL。例如,在我的代码中,当我想要重新创建bundle时,我会执行以下操作:

// remove an existing bundle
BundleTable.Bundles.Remove(BundleTable.Bundles.GetBundleFor(bundleAlias));

// recreate it.
var bundle = new ScriptBundle(bundleAlias);

// dependencies is a collection of objects representing scripts, 
// this creates a new bundle from that list. 

foreach (var item in dependencies)
{
    bundle.Include(item.Path);
}

// add the new bundle to the collection

BundleTable.Bundles.Add(bundle);

// bundleAlias is the same alias used previously to create the bundle,
// like "~/mybundle1" 

var bundleUrl = BundleTable.Bundles.ResolveBundleUrl(bundleAlias);

// returns something like "/mybundle1?v=hzBkDmqVAC8R_Nme4OYZ5qoq5fLBIhAGguKa28lYLfQ1"
每当我删除并重新创建一个具有相同别名的bundle时,绝对不会发生任何事情:从ResolveBundleUrl返回的bundleUrl与我删除和重新创建bundle之前相同。这里所说的“相同”是指内容哈希值未更改以反映bundle的新内容。
编辑...实际上,情况比那更糟。bundle本身以某种方式被缓存在Bundles集合外。如果我只是生成自己的随机哈希以防止浏览器缓存脚本,则ASP.NET会返回旧脚本。因此,从BundleTable.Bundles中删除捆绑包实际上什么都没有做。
我可以简单地更改别名来解决这个问题,在开发中这是可以接受的,但我不喜欢这个想法,因为这意味着我必须在每次页面加载后弃用别名或者每次页面加载时BundleCollection都会增加大小。如果在生产环境中保留此设置,那将是一场灾难。
因此,似乎当服务于一个脚本时,它会独立于实际的BundleTables.Bundles对象被缓存。因此,如果您重新使用URL,即使在重复使用之前已经删除了其引用的bundle,它也会响应其缓存中的内容,并且更改Bundles对象不会刷新缓存 - 因此仅会使用新项(或者说具有不同名称的新项)。
这种行为看起来很奇怪……从集合中删除某些东西应该将其从缓存中删除。但是它没有。必须有一种方法可以清除此缓存并使用BundleCollection的当前内容,而不是在第一次访问该bundle时缓存的内容。
您有任何想法如何做到这一点吗?
存在一个未知目的的ResetAll方法,但它只会破坏事情,因此那不是解决方案。

这里有同样的问题。我想我已经成功解决了我的问题。你可以试着看看是否适用于你。完全同意。System.Web.Optimization的文档很糟糕,而且你在互联网上找到的所有示例都已过时。 - LeftyX
2
+1 是因为顶部有很好的参考资料,同时对微软对信任的期望进行了尖锐的评论。也因为提出了我想要答案的问题。 - Raif
6个回答

34

我们了解您在文档方面的痛点,不幸的是,这个功能仍然在快速更改中,并且生成文档有一些滞后性,几乎可以立即过时。 Rick的博客文章是最新的,我也尽力回答问题,以传播当前的信息。我们目前正在建立我们的官方codeplex网站,该网站将提供始终最新的文档。

现在谈到您特定的问题如何刷新bundles缓存。

  1. 我们使用由请求的bundle url生成的密钥将捆绑响应存储在ASP.NET缓存中,例如:Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"] 我们还针对用于生成此捆绑包的所有文件和目录设置缓存依赖项。因此,如果任何基础文件或目录发生更改,则会清除缓存条目。

  2. 我们并不真正支持每个请求动态更新BundleTable/BundleCollection。完全支持的场景是在应用程序启动期间配置束(这样在Web farm方案中一切都能正常工作,否则某些束请求可能会被发送到错误的服务器而导致404)。根据您的代码示例,我猜测您正在尝试在特定请求上动态修改bundle集合?任何类型的bundle管理/重新配置都应该伴随着一个AppDomain重置,以保证一切都已正确设置。

因此,请避免在不重新启动应用程序域的情况下修改捆绑定义。您可以自由地修改捆绑包中的实际文件,这应该会自动检测到并为您的捆绑包url生成新的哈希码。


2
谢谢您在这里分享您的专业知识!是的,我正在尝试动态修改捆绑集合。这些bundle是基于另一个脚本中描述的一组依赖项构建的(它本身不一定是bundle的一部分),这就是我为什么会遇到这个问题的原因。由于更改bundle中的脚本将强制刷新,因此可以完成此操作-是否有可能添加手动刷新方法?这并不是关键问题-这只是为了方便开发而已,但如果意外用于实际环境中容易造成问题,因此我不希望创建可能会导致问题的代码。 - Jamie Treworgy
1
缓存更新是延迟的,第一次使用捆绑包(通常通过引用捆绑包进行呈现)时,它将被添加到缓存中。如果您有一个等效的应用程序启动钩子,在处理请求之前在所有Web服务器上设置捆绑包,那就没问题了。 - Hao Kung
2
据我所知,这个不起作用。也就是说,如果我更改了组成文件,服务器缓存并没有像这里所述的那样被清除。你必须重新启动才能使任何更改生效。有人知道官方文档在哪里吗? - philw
请参考stackoverflow.com/a/34508048/1545567,以解决纯HTML的“我需要重新启动池”限制。 - Christophe Blin
如何处理以下情况:如果有一个数据库,例如从数据库中获取变量值,然后生成CSS文件。 - Talha Junaid
显示剩余6条评论

21
我有一个类似的问题。 在我的类BundleConfig中,我试图看看使用BundleTable.EnableOptimizations = true的效果是什么。
public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        BundleTable.EnableOptimizations = true;

        bundles.Add(...);
    }
}

一切都正常运作。
某个时候,我在进行调试并将属性设置为false。
我很难理解发生了什么,因为似乎无法解析和加载jquery的bundle(第一个)(/bundles/jquery?v=)。

在一些咒骂之后,我想我已经设法解决了问题。 尝试在注册开始处添加bundles.Clear()bundles.ResetAll(),然后事情应该能够重新开始工作。
public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Clear();
        bundles.ResetAll();

        BundleTable.EnableOptimizations = false;

        bundles.Add(...);
    }
}

我意识到只有在更改EnableOptimizations属性时才需要运行这两种方法。
更新: 深入挖掘后,我发现BundleTable.Bundles.ResolveBundleUrl@Scripts.Url似乎存在解析捆绑路径的问题。
为了简单起见,我添加了一些图片。

image 1

我已关闭优化并捆绑了几个脚本。

image 2

同样的包被包含在主体中。

image 3

@Scripts.Url 给我“优化”的捆绑路径,而 @Scripts.Render 生成正确的路径。
BundleTable.Bundles.ResolveBundleUrl 同样也是如此。

我正在使用 Visual Studio 2010 + MVC 4 + Framework .Net 4.0。


2
我刚试了一下,仍然没有清除缓存!我已经尝试过清除它、ResetAll,并且在启动时和需要重置缓存时都尝试将EnableOptimizations设置为false,但什么也没发生。唉。 - Jamie Treworgy
看到你的更新了...我需要尝试一下不使用ResolveBundleUrl。这太阴险了... - Jamie Treworgy
也许@Hao Kung可以尝试帮助我们理解。 - LeftyX
因为这非常有帮助,所以我点了个赞,但是我还不确定它是否正确,因为我的问题还没有完全解决! - Jamie Treworgy
6
这些方法的作用是什么:Scripts.Url只是BundleTable.Bundles.ResolveBundleUrl的别名,它还会解析非bundle url,因此它是一个通用的URL解析器,同时也知道bundles。Scripts.Render使用EnableOptimizations标志来确定是呈现对bundles的引用,还是组成bundle的组件。 - Hao Kung
显示剩余4条评论

8

记住好孔的建议,因为Web农场方案不要这样做,但我认为有很多情况下你可能想这样做。以下是一个解决方案:

BundleTable.Bundles.ResetAll(); //or something more specific if neccesary
var bundle = new Bundle("~/bundles/your-bundle-virtual-path");
//add your includes here or load them in from a config file

//this is where the magic happens
var context = new BundleContext(new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, bundle.Path);
bundle.UpdateCache(context, bundle.GenerateBundleResponse(context));

BundleTable.Bundles.Add(bundle);

您可以随时调用上述代码,您的捆绑包将得到更新。无论EnableOptimizations为true还是false都可以使用此功能 - 换句话说,在调试或实时场景中,这将清除正确的标记:

@Scripts.Render("~/bundles/your-bundle-virtual-path")

更多阅读内容请点击这里,其中讲到了缓存和GenerateBundleResponse - Zac

4
我也遇到了在不重新构建的情况下更新捆绑包的问题。以下是需要理解的重要内容:
  • 如果文件路径发生改变,捆绑包将不会得到更新。
  • 如果捆绑包的虚拟路径发生更改,捆绑包将会得到更新。
  • 如果磁盘上的文件发生更改,捆绑包将会得到更新。
因此,如果您正在进行动态打包,可以编写一些代码使捆绑包的虚拟路径基于文件路径。我建议对文件路径进行哈希处理,并将哈希值附加到捆绑包的虚拟路径末尾。这样,当文件路径发生变化时,虚拟路径也会发生变化,捆绑包将会更新。
这是我最终解决问题所使用的代码:
    public static IHtmlString RenderStyleBundle(string bundlePath, string[] filePaths)
    {
        // Add a hash of the files onto the path to ensure that the filepaths have not changed.
        bundlePath = string.Format("{0}{1}", bundlePath, GetBundleHashForFiles(filePaths));

        var bundleIsRegistered = BundleTable
            .Bundles
            .GetRegisteredBundles()
            .Where(bundle => bundle.Path == bundlePath)
            .Any();

        if(!bundleIsRegistered)
        {
            var bundle = new StyleBundle(bundlePath);
            bundle.Include(filePaths);
            BundleTable.Bundles.Add(bundle);
        }

        return Styles.Render(bundlePath);
    }

    static string GetBundleHashForFiles(IEnumerable<string> filePaths)
    {
        // Create a unique hash for this set of files
        var aggregatedPaths = filePaths.Aggregate((pathString, next) => pathString + next);
        var Md5 = MD5.Create();
        var encodedPaths = Encoding.UTF8.GetBytes(aggregatedPaths);
        var hash = Md5.ComputeHash(encodedPaths);
        var bundlePath = hash.Aggregate(string.Empty, (hashString, next) => string.Format("{0}{1:x2}", hashString, next));
        return bundlePath;
    }

我建议通常避免使用Aggregate进行字符串连接,因为存在风险,即有人没有考虑到重复使用+中固有的Schlemiel the Painter's algorithm。相反,只需使用string.Join("", filePaths)即可。即使输入非常大,也不会出现这个问题。 - ErikE

3

你尝试过从 (StyleBundleScriptBundle) 派生,构造函数中不添加任何包含项,然后重写该类。

public override IEnumerable<System.IO.FileInfo> EnumerateFiles(BundleContext context)

我这样做是为了动态样式表,而且每次请求都会调用EnumerateFiles。这可能不是最好的解决方案,但它能正常工作。


0

抱歉重新激活一个已经过时的帖子,但是我在一个Umbraco网站中遇到了类似的Bundle缓存问题,我希望当用户在后端更改漂亮版本时,样式表/脚本可以自动压缩。

我已经有的代码是(在样式表的onSaved方法中):

 BundleTable.Bundles.Add(new StyleBundle("~/bundles/styles.min.css").Include(
                           "~/css/main.css"
                        ));

并且(在应用程序启动时):

BundleTable.EnableOptimizations = true;

无论我尝试什么,"~/bundles/styles.min.css" 文件似乎都没有改变。在我的页面头部,我最初是这样加载样式表的:
<link rel="stylesheet" href="~/bundles/styles.min.css" />

然而,我通过将其更改为以下内容使其正常工作:

@Styles.Render("~/bundles/styles.min.css")

Styles.Render方法在文件名末尾提取一个查询字符串,我猜这就是Hao上面所描述的缓存键。

对我来说,这很简单。希望这能帮助像我一样搜索了几个小时并且只找到几年前的帖子的人!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接