web-dev-qa-db-fra.com

Comment forcer BundleCollection à vider les ensembles de scripts mis en cache dans MVC4

... ou comment j'ai appris à cesser de m'inquiéter et à écrire du code contre des API complètement non documentées de Microsoft. Existe-t-il une documentation réelle du fonctionnaire System.Web.Optimization Libération? 'Parce que je ne peux certainement pas en trouver, il n'y a pas de documentation XML, et tous les billets de blog font référence à l'API de RC qui est sensiblement différente. Anyhoo ..

J'écris du code pour résoudre automatiquement les dépendances javascript et je crée des bundles à la volée à partir de ces dépendances. Tout fonctionne correctement, sauf si vous modifiez des scripts ou apportez des modifications qui affectent un ensemble sans redémarrer l'application, les modifications ne seront pas reflétées. J'ai donc ajouté une option pour désactiver la mise en cache des dépendances à utiliser dans le développement.

Cependant, apparemment BundleTables met en cache l'URL même si la collection d'ensembles a été modifiée. Par exemple, dans mon propre code, lorsque je veux recréer un paquet, je fais quelque chose comme ceci:

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

Chaque fois que je retire et recrée un paquet avec le même alias, absolument rien ne se passe: le bundleUrl renvoyé de ResolveBundleUrl est identique à celui que j'avais avant de retirer et recréer le paquet. Par "le même", je veux dire que le hachage de contenu n'est pas modifié pour refléter le nouveau contenu du paquet.

edit ... en fait, c'est bien pire que ça. Le le paquet lui-même est mis en cache d'une manière ou d'une autre en dehors de la collection Bundles. Si je génère simplement mon propre hachage aléatoire pour empêcher le navigateur de mettre en cache le script, ASP.NET renvoie l'ancien script. Donc, apparemment, retirer un paquet de BundleTable.Bundles ne fait rien.

Je peux simplement changer le pseudonyme pour contourner ce problème, ce qui convient au développement, mais je n'aime pas cette idée, car cela signifie que je dois déprécier les pseudonymes après le chargement de chaque page ou disposer d'une BundleCollection dont la taille augmente. chaque page se charge. Si vous laissiez cela dans un environnement de production, ce serait un désastre.

Il semble donc que lorsqu'un script est servi, il est mis en cache indépendamment du fichier BundleTables.Bundles objet. Donc, si vous réutilisez une URL, même si vous avez supprimé le paquet auquel elle faisait référence avant de le réutiliser, il répond avec tout ce qui se trouve dans son cache, et modifier l'objet Bundles ne vide pas le cache - Ainsi, seuls nouveaux éléments (ou plutôt, de nouveaux éléments portant un nom différent) seraient jamais utilisés.

Le comportement semble étrange ... supprimer quelque chose de la collection devrait le retirer du cache. Mais ce n'est pas le cas. Il doit y avoir un moyen de vider ce cache et de le faire utiliser le contenu actuel du BundleCollection au lieu de ce qu'il a mis en cache lors du premier accès à cet ensemble.

Une idée de comment je ferais ça?

Il y a cette méthode ResetAll qui a un but inconnu, mais elle casse les choses de toute façon, alors ce n'est pas ça.

83
Jamie Treworgy

Nous entendons votre problème avec la documentation, malheureusement, cette fonctionnalité est encore en train de changer assez rapidement, et la génération de documentation a un certain retard et peut être périmée presque immédiatement. Le blog de Rick est à jour, et j'ai essayé de répondre aux questions ici aussi pour diffuser les informations actuelles entre-temps. Nous sommes actuellement en train de mettre en place notre site officiel codeplex qui aura toujours une documentation à jour.

Maintenant, en ce qui concerne votre question spécifique sur la façon de vider les ensembles du cache.

  1. Nous stockons la réponse fournie dans le cache ASP.NET à l'aide d'une clé générée à partir de l'URL de l'offre demandée, à savoir Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"] _ nous établissons également des dépendances de cache par rapport à tous les fichiers et répertoires utilisés pour générer cet ensemble. Ainsi, si l'un des fichiers ou des répertoires sous-jacents change, l'entrée de la mémoire cache sera vidée.

  2. Nous ne prenons pas vraiment en charge la mise à jour en direct de BundleTable/BundleCollection à la demande. Le scénario entièrement pris en charge est que les lots sont configurés au démarrage de l'application (pour que tout fonctionne correctement dans le scénario de batterie de serveurs Web, sinon certaines demandes de lot finiraient par être 404 si elles étaient envoyées au mauvais serveur). En regardant votre exemple de code, je suppose que vous essayez de modifier la collection d'ensembles de manière dynamique pour une requête particulière? Tout type d’administration/reconfiguration de paquet doit être accompagné d’une réinitialisation d’application pour garantir que tout a été correctement configuré.

Évitez donc de modifier les définitions de vos ensembles sans recycler votre domaine d'application. Vous êtes libre de modifier les fichiers réels à l'intérieur de vos ensembles, qui doivent être automatiquement détectés et générer de nouveaux codes de hachage pour les URL de vos ensembles.

33
Hao Kung

J'ai un problème similaire.
Dans ma classe BundleConfig j'essayais de voir quel était l'effet de l'utilisation de BundleTable.EnableOptimizations = true.

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

        bundles.Add(...);
    }
}

Tout fonctionnait bien.
À un moment donné, j’étais en train de déboguer et de définir la propriété sur false.
J'ai eu du mal à comprendre ce qui se passait car il semblait que le paquet pour jQuery (le premier) ne serait pas résolu et chargé (/bundles/jquery?v=).

Après quelques jurons je pense (?!) Que j'ai réussi à arranger les choses. Essayez d’ajouter bundles.Clear() et bundles.ResetAll() au début de l’enregistrement et tout devrait recommencer à fonctionner.

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

        BundleTable.EnableOptimizations = false;

        bundles.Add(...);
    }
}

J'ai réalisé que je devais exécuter ces deux méthodes uniquement lorsque je modifiais la propriété EnableOptimizations.

PDATE:

Pour aller plus loin, j'ai découvert que BundleTable.Bundles.ResolveBundleUrl Et @Scripts.Url Semblent avoir des problèmes pour résoudre le chemin du paquet.

Par souci de simplicité, j'ai ajouté quelques images:

image 1

J'ai désactivé l'optimisation et fourni quelques scripts.

image 2

Le même paquet est inclus dans le corps.

image 3

@Scripts.Url Me donne le chemin "optimisé" du paquet tandis que @Scripts.Render Génère le bon.
La même chose se produit avec BundleTable.Bundles.ResolveBundleUrl.

J'utilise Visual Studio 2010 + MVC 4 + Framework .Net 4.0.

20
LeftyX

Gardant à l'esprit les recommandations de Hao Kung de ne pas le faire en raison de scénarios de batterie de serveurs Web, je pense qu'il existe de nombreux scénarios pour lesquels vous pouvez le faire. Voici une 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);

Vous pouvez appeler le code ci-dessus à tout moment et vos offres seront mises à jour. Cela fonctionne à la fois lorsque EnableOptimizations est vrai ou faux - autrement dit, cela jettera le balisage correct dans les scénarios de débogage ou en direct, avec:

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

J'ai également rencontré des problèmes avec la mise à jour des ensembles sans reconstruction. Voici les choses importantes à comprendre:

  • Le paquet ne se met pas à jour si les chemins de fichiers changent.
  • L'ensemble est mis à jour si le chemin virtuel de l'ensemble est modifié.
  • L'ensemble est mis à jour si les fichiers sur le disque changent.

En sachant que, si vous utilisez un regroupement dynamique, vous pouvez écrire du code pour que le chemin virtuel du groupe soit basé sur les chemins de fichier. Je recommande de hacher les chemins de fichiers et d'ajouter ce hachage à la fin du chemin virtuel de l'ensemble. De cette façon, lorsque les chemins de fichiers changent, le chemin virtuel change et l'ensemble est mis à jour.

Voici le code qui m'a permis de résoudre le problè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;
    }
4
FriendScottN

Avez-vous essayé de dériver à partir de (StyleBundle ou ScriptBundle), en n’ajoutant aucune inclusion dans votre constructeur, puis en remplaçant

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

Je le fais pour les feuilles de style dynamiques et EnumerateFiles est appelée à chaque demande. Ce n'est probablement pas la meilleure solution, mais cela fonctionne.

3
tulde23

Toutes mes excuses pour faire revivre un fil mort, mais j’ai rencontré un problème similaire avec la mise en cache Bundle dans un site Umbraco où je voulais que les feuilles de style/scripts s’agrandissent automatiquement lorsque l’utilisateur modifie la jolie version dans le backend.

Le code que j'avais déjà était (dans la méthode onSaved pour la feuille de style):

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

et (onApplicationStarted):

BundleTable.EnableOptimizations = true;

Peu importe ce que j'ai essayé, le fichier "~/bundles/styles.min.css" n'a pas semblé changer. Dans l'en-tête de ma page, je chargeais à l'origine dans la feuille de style de la manière suivante:

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

Cependant, j'ai réussi à changer cela en:

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

La méthode Styles.Render extrait une chaîne de requête à la fin du nom de fichier, ce qui, je suppose, est la clé de cache décrite par Hao ci-dessus.

Pour moi, c'était aussi simple que cela. J'espère que cela aidera quelqu'un comme moi qui googe pendant des heures et ne trouve que des messages de plusieurs années!

0
SY6Dave