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

85

... ou comment j'ai appris à cesser de m'inquiéter et à écrire simplement du code contre des API complètement non documentées de Microsoft . Existe-t-il une documentation réelle de la System.Web.Optimizationversion officielle ? Parce que je ne peux certainement pas en trouver, il n'y a pas de documents XML et tous les articles de blog font référence à l'API 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 très bien, sauf si vous modifiez des scripts ou apportez des modifications qui affectent un bundle 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 BundleTablesmet en cache l'URL même si la collection de bundles a changé . Par exemple, dans mon propre code, lorsque je souhaite recréer un bundle, 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 supprime et recrée un bundle avec le même alias , absolument rien ne se passe: le bundleUrlretour de ResolveBundleUrlest le même qu'avant de supprimer et recréer le bundle. Par «le même», je veux dire que le hachage du contenu est inchangé pour refléter le nouveau contenu du bundle.

modifier ... en fait, c'est bien pire que ça. Le bundle lui - même est mis en cache en dehors de la Bundlescollection. 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, supprimer un bundle de BundleTable.Bundlesne fait rien.

Je peux simplement changer l'alias pour contourner ce problème, et c'est OK pour le développement, mais je n'aime pas cette idée car cela signifie soit que je dois déprécier les alias après chaque chargement de page, soit avoir un BundleCollection qui grossit sur chaque chargement de page. Si vous laissiez cela activé 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 de l' BundleTables.Bundlesobjet réel . Donc, si vous réutilisez une URL, même si vous avez supprimé le bundle auquel elle faisait référence avant de la réutiliser, elle répond avec ce qui se trouve dans son cache, et la modification de l' Bundlesobjet ne vide pas le cache - donc seuls les nouveaux éléments (ou plutôt, de nouveaux éléments avec un nom différent) seraient jamais utilisés.

Le comportement semble étrange ... supprimer quelque chose de la collection devrait le supprimer du cache. Mais ce n'est pas le cas. Il doit y avoir un moyen de vider ce cache et de lui faire utiliser le contenu actuel du BundleCollectionplutôt que ce qu'il a mis en cache lors du premier accès à ce bundle.

Une idée de comment je ferais ça?

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

Jamie Treworgy
la source
Même problème ici. Je pense avoir réussi à résoudre le mien. Essayez et regardez si cela fonctionne pour vous. Entièrement d'accord. La documentation pour System.Web.Optimization est nul et tous les exemples que vous pouvez trouver sur Internet sont obsolètes.
LeftyX
2
+1 pour une excellente référence en haut combiné avec un commentaire mordant sur l'attente de confiance de MS. Et aussi pour avoir posé la question à laquelle je veux une réponse.
Raif

Réponses:

33

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

Maintenant, en ce qui concerne votre problème spécifique de savoir comment vider les bundles du cache.

  1. Nous stockons la réponse groupée à l'intérieur du cache ASP.NET en utilisant une clé générée à partir de l'url du bundle demandée, c'est-à-dire que Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"]nous configurons également des dépendances de cache contre tous les fichiers et répertoires qui ont été utilisés pour générer ce bundle. Donc, si l'un des fichiers ou répertoires sous-jacents change, l'entrée du cache sera vidée.

  2. Nous ne prenons pas vraiment en charge la mise à jour en direct de BundleTable / BundleCollection sur une base par demande. Le scénario entièrement pris en charge est que les bundles sont configurés lors du démarrage de l'application (c'est pour que tout fonctionne correctement dans le scénario de la batterie de serveurs Web, sinon certaines demandes de bundle 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 dynamiquement la collection de bundles sur une demande particulière? Tout type d'administration / reconfiguration de bundle doit être accompagné d'une réinitialisation du domaine d'application pour garantir que tout a été configuré correctement.

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

Hao Kung
la source
2
merci d'avoir apporté vos connaissances directes ici! Oui, j'essaye de modifier dynamiquement la collection de bundles. Les bundles sont construits sur la base d'un ensemble de dépendances décrites dans un autre script (qui ne fait pas nécessairement partie du bundle) - c'est pourquoi je rencontre ce problème. Étant donné que la modification d'un script qui se trouve dans un bundle forcera un vidage, cela peut être fait - y a-t-il une possibilité d'ajouter une méthode de vidage manuel? Ce n'est pas crucial - c'est pour plus de commodité pendant le développement - mais je déteste créer du code qui pourrait causer des problèmes s'il était utilisé accidentellement sur prod.
Jamie Treworgy
Pouvez-vous également élaborer sur le problème de la ferme Web? L'ajout d'un nouveau bundle après le démarrage de l'application entraînerait-il qu'il ne soit disponible que sur le serveur sur lequel il a été créé - ou essaie simplement de changer un existant? Ce serait un peu un dealkiller pour ce que j'essaie de faire car il doit faire la résolution d'exécution des dépendances.
Jamie Treworgy
Bien sûr, nous pourrions ajouter une méthode équivalente de vidage de cache explicite, elle existe déjà en interne. En ce qui concerne le problème de la batterie de serveurs Web, imaginez que vous avez deux serveurs Web A et B, votre demande va à A qui ajoute le bundle et envoie la réponse, votre client va maintenant chercher le contenu du bundle, mais oups la demande va à serveur B qui n'a pas enregistré le bundle, et voici votre 404.
Hao Kung
1
La mise à jour du cache est paresseuse, la première fois que le bundle est utilisé (généralement via le rendu d'une référence au bundle), il est ajouté au cache. Si vous avez un hook de démarrage d'application équivalent dans lequel vous configurez vos bundles sur tous les serveurs Web avant de commencer à gérer les demandes, cela devrait bien se passer.
Hao Kung
2
Autant que je sache, cela ne fonctionne pas. Autrement dit, si je change le ou les fichiers constituants, le cache du serveur n'est pas effacé comme indiqué ici. Vous devez recycler la chose pour obtenir des changements. Quelqu'un sait-il où se trouve réellement cette documentation officielle?
philw
21

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

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

        bundles.Add(...);
    }
}

Tout fonctionnait bien.
À un moment donné, je faisais du débogage et définissais la propriété sur false.
J'ai eu du mal à comprendre ce qui se passait car il semblait que le bundle 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 les choses devraient 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 EnableOptimizationspropriété.

MISE À JOUR:

En creusant plus profondément, j'ai découvert cela BundleTable.Bundles.ResolveBundleUrlet @Scripts.Urlsemble avoir des problèmes pour résoudre le chemin du bundle.

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

image 1

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

image 2

Le même paquet est inclus dans le corps.

image 3

@Scripts.Urlme donne le chemin "optimisé" du bundle tandis que @Scripts.Rendergénère le chemin approprié.
La même chose arrive avec BundleTable.Bundles.ResolveBundleUrl.

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

LeftyX
la source
Hmm ... le fait est que je ne veux pas effacer la table des bundles, car elle en contiendra beaucoup d'autres à partir de différentes pages (créées à partir de différents ensembles de dépendances). Mais comme c'est vraiment juste pour travailler dans un environnement de développement, je pense que je pourrais en copier le contenu, puis l'effacer, puis les ajouter à nouveau, si cela vidait le cache. Horriblement inefficace mais si cela fonctionne, c'est assez bon pour le développement.
Jamie Treworgy
D'accord, mais c'est la seule option que j'avais. J'ai passé tout l'après-midi à essayer de comprendre quel était le problème.
LeftyX
2
Je viens de l'essayer, toujours pas vider le cache !! Je l'efface ResetAllet j'ai essayé de régler EnableOptimizationssur false à la fois au démarrage et en ligne lorsque j'ai besoin de réinitialiser le cache, rien ne se passe. Argh.
Jamie Treworgy
Ce serait bien si le développeur pouvait lancer un article de blog rapide avec même une seule ligne sur les méthodes de ces objets :)
Jamie Treworgy
6
Donc, juste pour expliquer ce que font ces méthodes: Scripts.Url est juste un alias pour BundleTable.Bundles.ResolveBundleUrl, il résoudra également les URL non groupées, donc c'est un résolveur d'URL générique qui connaît les bundles. Scripts.Render utilise l'indicateur EnableOptimizations pour déterminer s'il faut restituer une référence aux bundles ou aux composants qui composent le bundle.
Hao Kung
8

En gardant à l'esprit les recommandations de Hao Kung de ne pas faire cela à cause des scénarios de ferme Web, je pense qu'il existe de nombreux scénarios dans lesquels vous pourriez vouloir faire cela. 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 forfaits seront mis à jour. Cela fonctionne à la fois lorsque EnableOptimizations est vrai ou faux - en d'autres termes, cela jettera le balisage correct dans les scénarios de débogage ou en direct, avec:

@Scripts.Render("~/bundles/your-bundle-virtual-path")
Zac
la source
Lire plus ici qui parle un peu de la mise en cache etGenerateBundleResponse
Zac
4

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

  • Le bundle N'EST PAS mis à jour si les chemins de fichiers changent.
  • Le bundle est mis à jour si le chemin virtuel du bundle change.
  • Le bundle est mis à jour si les fichiers sur le disque changent.

Donc, sachant que, si vous effectuez un regroupement dynamique, vous pouvez écrire du code pour que le chemin virtuel du bundle soit basé sur les chemins de fichiers. Je recommande de hacher les chemins de fichiers et d'ajouter ce hachage à la fin du chemin virtuel du bundle. De cette façon, lorsque les chemins de fichiers changent, le chemin virtuel change également et le bundle se met à jour.

Voici le code avec lequel je me suis retrouvé et qui a résolu le problème pour moi:

    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;
    }
FriendScottN
la source
Je recommande généralement d'éviter Aggregatela concaténation de chaînes, en raison du risque que quelqu'un ne pense pas à l' algorithme intrinsèque de Schlemiel le peintre en utilisant à plusieurs reprises +. Au lieu de cela, faites-le string.Join("", filePaths). Cela n'aura pas ce problème, même pour les très gros intrants.
ErikE
3

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

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

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

tulde23
la source
0

Toutes mes excuses pour raviver un fil mort, mais j'ai rencontré un problème similaire avec la mise en cache de Bundle dans un site Umbraco où je voulais que les feuilles de style / scripts soient automatiquement réduits lorsque l'utilisateur a changé 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 la tête de ma page, je chargeais à l'origine dans la feuille de style comme ceci:

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

Cependant, je l'ai fait fonctionner en changeant 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 qui, je suppose, est la clé de cache décrite par Hao ci-dessus.

Pour moi, c'était aussi simple que ça. J'espère que cela aide quelqu'un d'autre comme moi qui a cherché sur Google pendant des heures et qui n'a pu trouver que des messages vieux de plusieurs années!

SY6Dave
la source