Как заставить BundleCollection сбрасывать кешированные пакеты скриптов в MVC4

85

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

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

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

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

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

Есть идеи, как бы я это сделал?

Есть ResetAllметод, цель которого неизвестна, но он все равно ломает вещи, так что это не так.

Джейми Треворги
источник
Здесь та же проблема. Думаю, мне удалось решить свою. Попробуйте и посмотрите, работает ли это для вас. Полностью согласен. Документация для System.Web.Optimization - мусор, а все образцы, которые можно найти в Интернете, устарели.
LeftyX
2
+1 за отличную ссылку вверху в сочетании с едким комментарием об ожидании доверия от MS. А также за то, что задаю вопрос, на который хочу получить ответ.
Raif

Ответы:

33

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

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

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

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

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

Хао Кунг
источник
2
Спасибо, что принесли сюда свои непосредственные знания! Да, я пытаюсь динамически изменять коллекцию пакетов. Пакеты построены на основе набора зависимостей, описанных в другом скрипте (то есть сам по себе, не обязательно часть пакета) - вот почему у меня возникла эта проблема. Поскольку изменение скрипта, находящегося в пакете, вызовет сброс, это можно сделать - есть ли возможность добавить метод ручного сброса? Это не критично - это для удобства во время разработки, - но я ненавижу создавать код, который может вызвать проблемы при случайном использовании в продукте.
Джейми Треворги,
Не могли бы вы подробнее рассказать о веб-ферме? Приведет ли добавление нового пакета после запуска приложения к тому, что он будет доступен только на сервере, на котором он был создан, или просто попытается изменить существующий? Это было бы чем-то вроде того, что я пытаюсь сделать, поскольку ему нужно выполнять разрешение зависимостей во время выполнения.
Джейми Трюорги,
Конечно, мы могли бы добавить явный эквивалентный метод очистки кеша, он уже есть внутри. Что касается проблемы веб-фермы, в основном представьте, что у вас есть два веб-сервера A и B, ваш запрос переходит к A, который добавляет пакет и отправляет ответ, ваш клиент теперь отправляется за содержимым пакета, но, к сожалению, запрос отправляется на сервер B, который не зарегистрировал бандл, и вот ваш 404.
Хао Кунг,
1
Обновление кеша выполняется лениво, при первом использовании пакета (обычно путем рендеринга ссылки на пакет) он добавляется в кеш. Если у вас есть эквивалентный запуск приложения, в котором вы настраиваете свои пакеты на всех веб-серверах до того, как начнете обрабатывать запросы, это должно быть нормально.
Хао Кунг,
2
Насколько я могу судить, это не работает. То есть, если я изменяю составляющие файлы, кеш сервера не очищается, как указано здесь. Вы должны утилизировать вещь, чтобы внести какие-либо изменения. Кто-нибудь знает, где на самом деле находится эта официальная документация?
philw
21

У меня похожая проблема.
В моем классе 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свойства.

ОБНОВИТЬ:

Копая глубже, я обнаружил это BundleTable.Bundles.ResolveBundleUrlи, @Scripts.Urlпохоже, у меня проблемы с разрешением пути к пакету.

Для простоты я добавил несколько изображений:

изображение 1

Я отключил оптимизацию и собрал несколько скриптов.

изображение 2

Такой же комплект входит в корпус.

изображение 3

@Scripts.Urlдает мне "оптимизированный" путь пакета, в то время как @Scripts.Renderгенерирует правильный.
То же самое происходит с BundleTable.Bundles.ResolveBundleUrl.

Я использую Visual Studio 2010 + MVC 4 + Framework .Net 4.0.

LeftyX
источник
Хм ... Дело в том, что я на самом деле не хочу очищать таблицу пакетов, потому что она будет содержать множество других с разных страниц (созданных из разных наборов зависимостей). Но поскольку это действительно только для работы в среде разработки, я думаю, что могу скопировать его содержимое, затем очистить его, а затем добавить их снова, если это очистит кеш. Ужасно неэффективно, но если это сработает, этого достаточно для разработчиков.
Джейми Треворги,
Согласитесь, но это единственный вариант, который у меня был. Я потратил весь день, пытаясь понять, в чем проблема.
LeftyX 07
2
Я только что попробовал, ВСЕ ЕЩЕ не промывает кеш !! Я очищаю его, ResetAllи пробовал установить EnableOptimizationsзначение false как при запуске, так и при встроенном, когда мне нужно сбросить кеш, ничего не происходит. Ага.
Джейми Треворги,
Было бы неплохо, если бы разработчик мог быстро
написать в
6
Итак, просто чтобы объяснить, что делают эти методы: Scripts.Url - это просто псевдоним для BundleTable.Bundles.ResolveBundleUrl, он также будет разрешать URL-адреса, не связанные с пакетами, поэтому это общий преобразователь URL-адресов, который, как известно, знает о пакетах. Scripts.Render использует флаг EnableOptimizations, чтобы определить, отображать ли ссылку на пакеты или на компоненты, составляющие пакет.
Хао Кунг
8

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

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;
    }
ДругScottN
источник
Я рекомендую обычно избегать Aggregateконкатенации строк из-за риска того, что кто-то не подумает о многократно используемом алгоритме Шлемиля-художника+ . Вместо этого просто делай string.Join("", filePaths). У этого не будет этой проблемы даже для очень больших входов.
ErikE
3

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

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

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

Tulde23
источник
0

Приносим извинения за возрождение мертвого потока, однако я столкнулся с аналогичной проблемой с кешированием Bundle на сайте Umbraco, где я хотел, чтобы таблицы стилей / скрипты автоматически уменьшались, когда пользователь изменял красивую версию в бэкэнде.

Код, который у меня уже был (в методе onSaved для таблицы стилей):

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

и (onApplicationStarted):

BundleTable.EnableOptimizations = true;

Что бы я ни пробовал, файл "~ / bundles / styles.min.css", похоже, не изменился. Изначально в заголовке страницы я загружал таблицу стилей так:

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

Однако я заставил его работать, изменив это на:

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

Метод Styles.Render вытягивает строку запроса в конце имени файла, которое, как я предполагаю, является ключом кеша, описанным Хао выше.

Для меня это было так просто. Надеюсь, это поможет любому, как я, кто часами гуглил и мог найти только сообщения нескольких лет назад!

SY6Дэйв
источник