Как заставить BundleCollection выгрузить кэшированные пакеты script в MVC4

... или как я научился перестать беспокоиться и просто написать код против полностью недокументированных API от Microsoft. Есть ли фактическая документация официального релиза System.Web.Optimization? "Потому что я уверен, что не могу найти ни одного документа XML, и все сообщения в блогах относятся к RC API, который существенно отличается. Anyhoo..

Я пишу какой-то код для автоматического разрешения зависимостей javascript и создаю пакеты на лету из этих зависимостей. Все отлично работает, за исключением случаев, когда вы редактируете скрипты или иным образом вносите изменения, которые влияют на пакет без перезапуска приложения, изменения не будут отображаться. Поэтому я добавил вариант отключения кэширования зависимостей для использования в разработке.

Однако, по-видимому, BundleTables кэширует URL-адрес, даже если коллекция пакетов изменилась. Например, в моем собственном коде, когда я хочу воссоздать пакет, я делаю что-то вроде этого:

// 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"

Всякий раз, когда я удаляю и воссоздаю связку с тем же псевдонимом, абсолютно ничего не происходит: bundleUrl, возвращенный из ResolveBundleUrl, такой же, как и до того, как я удалил и воссоздал пакет. Под "тем же" я подразумеваю, что хэш содержимого не изменяется, чтобы отразить новое содержимое пакета.

edit... На самом деле это намного хуже. Сам комплект кэшируется как-то вне коллекции Bundles. Если я просто создаю свой собственный случайный хэш, чтобы предотвратить кеширование браузера script, ASP.NET возвращает старый script. Таким образом, по-видимому, удаление пакета из BundleTable.Bundles фактически ничего не делает.

Я могу просто изменить псевдоним, чтобы обойти эту проблему, и это нормально для разработки, но мне эта идея не нравится, поскольку это означает, что я должен отказаться от псевдонимов после каждой загрузки страницы или создать BundleCollection в размере при каждой загрузке страницы. Если вы оставите это в рабочей среде, это будет катастрофой.

Таким образом, кажется, что при обслуживании script он становится кэшированным независимо от фактического объекта BundleTables.Bundles. Поэтому, если вы повторно используете URL-адрес, даже если вы удалили связанный с ним пакет, который он ссылался, прежде чем повторно использовать его, он отвечает на все в своем кеше, а изменение объекта Bundles не очищает кеш - так что только новые элементы (или, скорее, новые элементы с другим именем) будут использоваться.

Поведение кажется странным... удаление чего-то из коллекции должно удалить его из кеша. Но это не так. Должен быть способ сбросить этот кеш и использовать его текущее содержимое BundleCollection вместо того, что он кэшировал, когда этот пакет был первым.

Любая идея, как я это сделаю?

Существует этот ResetAll метод, который имеет неизвестную цель, но он просто нарушает все, так что это не так.

Ответ 1

Мы слышим вашу боль в документации, к сожалению, эта функция все еще меняется довольно быстро, и создание документации имеет некоторое отставание и может быть устаревшим почти сразу. Сообщение в блоге Рика обновлено, и я попытался ответить на вопросы здесь, а также распространить текущую информацию за это время. В настоящее время мы создаем наш официальный сайт Codeplex, который будет всегда иметь текущую документацию.

Теперь в отношении вашей конкретной проблемы о том, как очистить пакеты от кеша.

  • Мы храним связанный ответ внутри кеша ASP.NET, используя ключ, сгенерированный с запрошенного url-пакета, т.е. Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"] мы также настраиваем зависимости кэша от всех файлов и каталогов, которые были использованы для генерации это расслоение. Поэтому, если какой-либо из основных файлов или каталогов изменится, запись в кэш будет сброшена.

  • Мы не поддерживаем текущее обновление BundleTable/BundleCollection для каждого запроса. Полностью поддерживаемый сценарий заключается в том, что пакеты настроены во время запуска приложения (это значит, что все работает правильно в сценарии веб-фермы, в противном случае некоторые запросы пакета будут в 404 раза отправлены на неправильный сервер). Глядя на ваш пример кода, я предполагаю, что вы пытаетесь модифицировать коллекцию пакетов динамически по конкретному запросу? Любое администрирование/реконфигурация пакета должно сопровождаться appdomain reset, чтобы гарантировать, что все настроено правильно.

Поэтому избегайте модификации определений ваших пакетов без повторной переработки вашего домена приложения. Вы можете изменять фактические файлы внутри своих пакетов, которые должны автоматически обнаруживаться и генерировать новые хэш-коды для ваших URL-адресов.

Ответ 2

У меня есть аналогичная проблема.
В моем классе BundleConfig я пытался понять, что было результатом использования BundleTable.EnableOptimizations = true.

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

        bundles.Add(...);
    }
}

Все отлично работает.
В какой-то момент я делал некоторую отладку и устанавливал свойство false. Я изо всех сил пытался понять, что происходит, потому что казалось, что пакет для jquery (первый) не будет разрешен и загружен (/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.

UPDATE:

Копая глубже, я обнаружил, что 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.

Ответ 3

Принимая во внимание рекомендации Хао Кунга, чтобы не делать этого из-за сценариев веб-фермы, я думаю, что есть много сценариев, где вы можете это сделать. Вот решение:

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 является истинным или ложным - другими словами, это выкинет правильную разметку в отладочных или живых сценариях, используя:

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

Ответ 4

Я также столкнулся с проблемами с обновлением пакетов без перестройки. Вот важные вещи для понимания:

  • Пакет НЕ обновляется, если путь к файлу изменяется.
  • Пакет DOES обновляется, если изменяется виртуальный путь пакета.
  • Пакет DOES обновляется, если файлы на диске меняются.

Итак, зная, что если вы выполняете динамическое связывание, вы можете написать код, чтобы виртуальный путь пакета был основан на пути к файлам. Я рекомендую хешировать пути к файлу и добавлять этот хэш в конец виртуального пути пакета. Таким образом, когда изменяются пути к файлу, так будет обновляться виртуальный путь и пакет.

Вот код, в котором я оказался, решил проблему для меня:

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

Ответ 5

Вы пытались извлечь из (StyleBundle или ScriptBundle), не добавляя никаких включений в свой конструктор, а затем переопределяя

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

Я делаю это для динамических таблиц стилей, и EnumerateFiles вызывается по каждому запросу. Это, вероятно, не самое большое решение, но оно работает.