如何强制 BundleCollection 在 MVC4中刷新缓存的脚本包

... or 我是如何学会不再担心,而只是针对微软完全没有文档记录的 API 编写代码的. Is there any actual documentation of the official System.Web.Optimization release? 'cuz I sure can't find any, there's no XML docs, and all the blog posts refer to the RC API which is substantially different. Anyhoo..

我正在编写一些代码来自动解决 javascript 依赖关系,并且正在动态地从这些依赖关系中创建包。除了在不重新启动应用程序的情况下编辑脚本或做出会影响捆绑包的更改外,其他操作都可以很好地工作,这些更改将不会被反映出来。因此,我添加了一个选项来禁用在开发中使用的依赖项的缓存。

但是,显然 BundleTables缓存了 URL 即使 bundle 集合已经更改。例如,在我自己的代码中,当我想重新创建一个 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"

每当我删除并重新创建一个包 用同样的化名,绝对没有任何事情发生: 从 ResolveBundleUrl返回的 bundleUrl与我删除并重新创建这个包之前是一样的。我所说的“相同”是指内容散列不变,以反映捆绑包的新内容。

编辑 ... 实际上,情况要比这糟糕得多。捆绑自己以某种方式缓存在 Bundles集合之外。如果我只是生成自己的随机散列来阻止浏览器缓存脚本,ASP.NET 将返回 旧剧本。因此,显然,从 BundleTable.Bundles中移除一个包实际上不会做任何事情。

我可以简单地改变别名来解决这个问题,这对于开发来说是可以的,但是我不喜欢这个想法,因为这意味着在每次页面加载之后我不得不放弃别名,或者有一个 BundleCollection,在每次页面加载之后它的大小都会增加。如果在生产环境中保留这个选项,那将是一场灾难。

因此,当提供一个脚本时,它似乎独立于实际的 BundleTables.Bundles对象被缓存。因此,如果你重用一个 URL,即使你在重用它之前删除了它所引用的捆绑包,它也会响应其缓存中的任何内容,并且改变 Bundles对象不会刷新缓存——所以只有 新的条目(或者更确切地说,不同名称的新条目)会被使用。

这种行为似乎很奇怪... 从集合中删除某些东西应该从缓存中删除它。但事实并非如此。必须有一种方法来刷新这个缓存,并让它使用 BundleCollection的当前内容,而不是在首次访问该包时缓存的内容。

你知道我会怎么做吗?

有这样一个 ResetAll方法,它有一个未知的用途,但它只是破坏事物,所以这不是它。

65023 次浏览

I've got a similar problem.
In my class BundleConfig I was trying to see what was the effect of using BundleTable.EnableOptimizations = true.

public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
BundleTable.EnableOptimizations = true;


bundles.Add(...);
}
}

Everything was working fine.
At some point I was doing some debugging and set the property to false.
I struggled to understand what was happening cause it seemed that the bundle for jquery (the first one) wouldn't be resolved and loaded (/bundles/jquery?v=).

After some swearing I think(?!) I've managed to sort things out. Try to add bundles.Clear() and bundles.ResetAll() at the beginning of the registration and things should start to work again.

public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Clear();
bundles.ResetAll();


BundleTable.EnableOptimizations = false;


bundles.Add(...);
}
}

I've realized I need to run these two methods only when I change the EnableOptimizations property.

UPDATE:

Digging deeper I've found out that BundleTable.Bundles.ResolveBundleUrl and @Scripts.Url seem to have problems to resolve the bundle path.

For sake of simplicity I've added a few images:

image 1

I have turned off the optimization and bundled a few scripts.

image 2

The same bundle is included in the body.

image 3

@Scripts.Url gives me the "optimized" path of the bundle while @Scripts.Render generates the proper one.
Same thing happens with BundleTable.Bundles.ResolveBundleUrl.

I am using Visual Studio 2010 + MVC 4 + Framework .Net 4.0.

We hear your pain on documentation, unfortunately this feature is still changing quite fast, and generating documentation has some lag, and can be outdated almost immediately. Rick's blog post is up to date, and I've tried to answer questions here as well to spread current info in the meantime. We are currently in the process of setting up our official codeplex site which will have always current documentation.

Now in regards to your specific issue of how to flush bundles form the cache.

  1. We store the bundled response inside of the ASP.NET cache using a key generated off of the bundle url requested, i.e. Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"] we also setup cache dependencies against all of the files and directories that were used to generate this bundle. So if any of the underlying files or directories change, the cache entry will get flushed.

  2. We don't really support live updating of the BundleTable/BundleCollection on a per request basis. The fully supported scenario is that bundles are configured during app start(this is so everything works properly in the web farm scenario, otherwise some bundle requests would end up being 404's if sent to the wrong server). Looking at your code example, my guess is that you are trying to modify the bundle collection dynamically on a particular request? Any kind of bundle administration/reconfiguration should be accompanied by an appdomain reset to guarantee everything has been setup correctly.

So avoid modifying your bundle definitions without recycling your app domain. You are free to modify the actual files inside of your bundles, that should automatically be detected and generate new hashcodes for your bundle urls.

Have you tried deriving from (StyleBundle or ScriptBundle), adding no inclusions in your constructor and then overriding

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

I do this for dynamic style sheets and EnumerateFiles gets called on every request. It's probably not the greatest solution but it works.

Bearing in mind Hao Kung's recommendations to not do this because of web farm scenarios, I think there are a lot of scenarios where you might want to do this. Here is a solution:

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);

You can call the above code at any time and your bundles will get updated. This works both when EnableOptimizations is true or false - in other words, this will throw out the correct markup in debug or live scenarios, with:

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

I also ran into issues with updating bundles without rebuilding. Here are the important things to understand:

  • The bundle DOES NOT get updated if the file paths change.
  • The bundle DOES get updated if the bundle's virtual path changes.
  • The bundle DOES get updated if the files on disk change.

So knowing that, if you're doing dynamic bundling, you can write some code to make the bundle's virtual path be based on the file paths. I recommend hashing the file paths and appending that hash to the end of the bundle's virtual path. This way when the file paths change so does the virtual path and the bundle will update.

Here's the code I ended up with that solved the issue for me:

    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;
}

Apologies to revive a dead thread, however I ran into a similar issue with Bundle caching in an Umbraco site where I wanted the stylesheets/scripts to automatically minify when the user changed the pretty version in the backend.

The code I already had was (in the onSaved method for the stylesheet):

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

and (onApplicationStarted):

BundleTable.EnableOptimizations = true;

No matter what I tried, the "~/bundles/styles.min.css" file didn't seem to change. In the head of my page, I was originally loading in the stylesheet like so:

<link rel="stylesheet" href="~/bundles/styles.min.css" />

However, I got it to work by changing this to:

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

The Styles.Render method pulls in a query string at the end of the file name which I am guessing is the cache key described by Hao above.

For me, it was as simple as that. Hope this helps anyone else like me who was googling this for hours and could only find several year old posts!