Come forzare BundleCollection a svuotare i bundle di script memorizzati nella cache in MVC4


85

... o come ho imparato a smettere di preoccuparmi e scrivere codice contro API Microsoft completamente non documentate . Esiste una documentazione effettiva del System.Web.Optimizationrilascio ufficiale ? Perché di sicuro non riesco a trovarne nessuno, non ci sono documenti XML e tutti i post del blog si riferiscono all'API RC che è sostanzialmente diversa. Anyhoo ..

Sto scrivendo del codice per risolvere automaticamente le dipendenze javascript e sto creando bundle al volo da quelle dipendenze. Tutto funziona alla grande, tranne se modifichi gli script o apporti in altro modo modifiche che potrebbero influire su un pacchetto senza riavviare l'applicazione, le modifiche non verranno riflesse. Quindi ho aggiunto un'opzione per disabilitare la memorizzazione nella cache delle dipendenze da utilizzare nello sviluppo.

Tuttavia, apparentemente BundleTablesmemorizza nella cache l'URL anche se la raccolta del bundle è cambiata . Ad esempio, nel mio codice quando voglio ricreare un bundle faccio qualcosa del genere:

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

Ogni volta che rimuovo e ricrei un bundle con lo stesso alias , non accade assolutamente nulla: il bundleUrlritorno da ResolveBundleUrlè lo stesso di prima che avessi rimosso e ricreato il bundle. Con "lo stesso" intendo che l'hash del contenuto è invariato per riflettere i nuovi contenuti del bundle.

modifica ... in realtà, è molto peggio di così. Il bundle stesso viene memorizzato nella cache in qualche modo al di fuori della Bundlesraccolta. Se generi semplicemente il mio hash casuale per impedire al browser di memorizzare nella cache lo script, ASP.NET restituisce il vecchio script . Quindi, apparentemente, la rimozione di un bundle da in BundleTable.Bundlesrealtà non fa nulla.

Posso semplicemente cambiare l'alias per aggirare questo problema, e questo va bene per lo sviluppo, ma non mi piace l'idea poiché significa che devo deprecare gli alias dopo ogni caricamento della pagina o avere un BundleCollection che cresce di dimensioni su ogni caricamento della pagina. Se lo lasciassi attivo in un ambiente di produzione, sarebbe un disastro.

Quindi sembra che quando uno script viene servito, viene memorizzato nella cache indipendentemente BundleTables.Bundlesdall'oggetto reale . Quindi, se riutilizzi un URL, anche se hai rimosso il pacchetto a cui si riferiva prima di riutilizzarlo, risponde con tutto ciò che è nella sua cache e la modifica Bundlesdell'oggetto non svuota la cache, quindi solo i nuovi elementi (o piuttosto, nuovi elementi con un nome diverso) sarebbero mai stati utilizzati.

Il comportamento sembra strano ... rimuovere qualcosa dalla raccolta dovrebbe rimuoverlo dalla cache. Ma non è così. Deve esserci un modo per svuotare questa cache e fare in modo che utilizzi il contenuto corrente di BundleCollectioninvece di quello che ha memorizzato nella cache al primo accesso a quel bundle.

Qualche idea su come lo farei?

C'è questo ResetAllmetodo che ha uno scopo sconosciuto ma rompe comunque le cose, quindi non è così.


Stesso problema qui. Penso di essere riuscito a risolvere il mio. Prova a dare un'occhiata se funziona per te. Completamente d'accordo. La documentazione per System.Web.Optimization è spazzatura e tutti i campioni che puoi trovare su Internet sono obsoleti.
LeftyX

2
+1 per un ottimo riferimento in alto combinato con un commento pungente sull'aspettativa di fiducia della SM. E anche per aver posto la domanda a cui voglio una risposta.
Raif

Risposte:


33

Sentiamo il tuo dolore sulla documentazione, sfortunatamente questa funzione sta ancora cambiando abbastanza velocemente e la generazione della documentazione ha un certo ritardo e può essere obsoleta quasi immediatamente. Il post sul blog di Rick è aggiornato e nel frattempo ho cercato di rispondere alle domande anche per diffondere informazioni aggiornate. Siamo attualmente in fase di creazione del nostro sito ufficiale di codeplex che avrà una documentazione sempre aggiornata.

Ora per quanto riguarda il tuo problema specifico su come svuotare i pacchetti dalla cache.

  1. Memorizziamo la risposta in bundle all'interno della cache ASP.NET utilizzando una chiave generata dall'URL del bundle richiesto, ovvero Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"]impostiamo anche le dipendenze della cache su tutti i file e le directory che sono stati utilizzati per generare questo bundle. Quindi, se uno qualsiasi dei file o delle directory sottostanti cambia, la voce della cache verrà cancellata.

  2. Non supportiamo davvero l'aggiornamento in tempo reale di BundleTable / BundleCollection su richiesta. Lo scenario completamente supportato è che i bundle siano configurati durante l'avvio dell'app (in questo modo tutto funziona correttamente nello scenario della web farm, altrimenti alcune richieste di bundle finirebbero per essere 404 se inviate al server sbagliato). Guardando il tuo esempio di codice, la mia ipotesi è che stai cercando di modificare la raccolta di bundle dinamicamente su una particolare richiesta? Qualsiasi tipo di amministrazione / riconfigurazione del bundle dovrebbe essere accompagnato da un ripristino del dominio dell'app per garantire che tutto sia stato impostato correttamente.

Quindi evita di modificare le definizioni del bundle senza riciclare il dominio dell'app. Sei libero di modificare i file effettivi all'interno dei tuoi bundle, che dovrebbero essere rilevati automaticamente e generare nuovi codici hash per gli URL dei tuoi bundle.


2
grazie per aver portato qui la tua conoscenza diretta! Sì, sto cercando di modificare dinamicamente la raccolta di bundle. I bundle sono costruiti sulla base di un insieme di dipendenze descritte in un altro script (cioè, esso stesso, non necessariamente parte del bundle) - motivo per cui ho questo problema. Poiché la modifica di uno script che si trova in un pacchetto forzerà un flush, può essere fatto: c'è la possibilità di aggiungere un metodo di flush manuale? Questo non è cruciale - è per comodità durante lo sviluppo - ma odio creare codice che potrebbe causare problemi se usato accidentalmente su prod.
Jamie Treworgy

Puoi anche approfondire il problema della web farm? L'aggiunta di un nuovo pacchetto dopo l'avvio dell'applicazione risulterebbe disponibile solo sul server su cui è stato creato o provando semplicemente a modificarne uno esistente? Questo sarebbe un po 'un affare per quello che sto cercando di fare poiché ha bisogno di fare la risoluzione in runtime delle dipendenze.
Jamie Treworgy

Certo, potremmo aggiungere un metodo equivalente allo scaricamento della cache esplicito, è già presente internamente. Per quanto riguarda il problema della web farm, in pratica immagina di avere due server web A e B, la tua richiesta va ad A che aggiunge il bundle e invia la risposta, il tuo client ora va a recuperare il contenuto del bundle, ma oops la richiesta va a server B che non ha registrato il pacchetto, e c'è il tuo 404.
Hao Kung

1
L'aggiornamento della cache è lento, la prima volta che il bundle viene utilizzato (in genere tramite il rendering di un riferimento al bundle), viene aggiunto alla cache. Se disponi di un'app equivalente, avvia hook in cui configuri i tuoi bundle su tutti i server web prima di iniziare a gestire le richieste, dovrebbe andare bene.
Hao Kung

2
Per quanto ne so, questo non funziona. Cioè, se cambio i file costituenti, la cache del server non viene cancellata come indicato qui. Devi riciclare la cosa per ottenere eventuali modifiche là fuori. Qualcuno sa dove sia effettivamente quella documentazione ufficiale?
philw

21

Ho un problema simile.
Nella mia classe BundleConfigstavo cercando di vedere quale fosse l'effetto dell'uso BundleTable.EnableOptimizations = true.

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

        bundles.Add(...);
    }
}

Tutto stava funzionando bene.
Ad un certo punto stavo facendo un po 'di debug e ho impostato la proprietà su false.
Ho faticato a capire cosa stesse succedendo perché sembrava che il bundle per jquery (il primo) non sarebbe stato risolto e caricato ( /bundles/jquery?v=).

Dopo alcune imprecazioni penso (?!) di essere riuscito a sistemare le cose. Prova ad aggiungere bundles.Clear()e bundles.ResetAll()all'inizio della registrazione e le cose dovrebbero ricominciare a funzionare.

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

        BundleTable.EnableOptimizations = false;

        bundles.Add(...);
    }
}

Mi sono reso conto che ho bisogno di eseguire questi due metodi solo quando cambio la EnableOptimizationsproprietà.

AGGIORNARE:

Scavando più a fondo, l'ho scoperto BundleTable.Bundles.ResolveBundleUrle @Scripts.Urlsembra che abbia problemi a risolvere il percorso del bundle.

Per semplicità ho aggiunto alcune immagini:

immagine 1

Ho disattivato l'ottimizzazione e ho raggruppato alcuni script.

immagine 2

Lo stesso pacchetto è incluso nel corpo.

immagine 3

@Scripts.Urlmi fornisce il percorso "ottimizzato" del bundle mentre @Scripts.Rendergenera quello corretto.
La stessa cosa accade con BundleTable.Bundles.ResolveBundleUrl.

Sto usando Visual Studio 2010 + MVC 4 + Framework .Net 4.0.


Hmm ... il fatto è che in realtà non voglio cancellare la tabella dei bundle, perché conterrà molti altri da pagine diverse (creati da diversi set di dipendenze). Ma poiché questo è davvero solo per lavorare in un ambiente di sviluppo, penso che potrei copiarne il contenuto, quindi cancellarlo, quindi aggiungerli di nuovo, se ciò svuotasse la cache. Orrendamente inefficiente ma se funziona, è abbastanza buono per dev.
Jamie Treworgy

D'accordo, ma questa è l'unica opzione che avevo. Ho passato tutto il pomeriggio cercando di capire quale fosse il problema.
LeftyX

2
L'ho appena provato, ANCORA non svuotare la cache !! Lo svuoto ResetAll, e ho provato a impostare EnableOptimizationssu false sia all'avvio che in linea quando ho bisogno di ripristinare la cache, non succede nulla. Argh.
Jamie Treworgy

Sarebbe sicuramente bello se lo sviluppatore potesse pubblicare un rapido post sul blog con anche una
frase

6
Quindi, solo per spiegare cosa fanno questi metodi: Scripts.Url è solo un alias per BundleTable.Bundles.ResolveBundleUrl, risolverà anche gli URL non bundle, quindi è un risolutore di URL generico che capita di conoscere i bundle. Scripts.Render utilizza il flag EnableOptimizations per determinare se eseguire il rendering di un riferimento ai bundle o ai componenti che compongono il bundle.
Hao Kung

8

Tenendo presente i consigli di Hao Kung di non farlo a causa di scenari di web farm, penso che ci siano molti scenari in cui potresti volerlo fare. Ecco una soluzione:

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

Puoi chiamare il codice sopra in qualsiasi momento e i tuoi pacchetti verranno aggiornati. Funziona sia quando EnableOptimizations è true o false, in altre parole, questo eliminerà il markup corretto in scenari di debug o live, con:

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

Ulteriore lettura qui che parla un po 'del caching eGenerateBundleResponse
Zac

4

Ho anche riscontrato problemi con l'aggiornamento dei bundle senza dover ricostruire. Ecco le cose importanti da capire:

  • Il bundle NON viene aggiornato se i percorsi dei file cambiano.
  • Il bundle viene aggiornato se il percorso virtuale del bundle cambia.
  • Il bundle viene aggiornato se i file sul disco cambiano.

Quindi sapendo che, se stai facendo il raggruppamento dinamico, puoi scrivere del codice per fare in modo che il percorso virtuale del pacchetto sia basato sui percorsi dei file. Consiglio di eseguire l'hashing dei percorsi dei file e di aggiungere tale hash alla fine del percorso virtuale del bundle. In questo modo, quando i percorsi dei file cambiano, cambia anche il percorso virtuale e il bundle si aggiornerà.

Ecco il codice che ho trovato che ha risolto il problema per 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;
    }

In genere consiglio di evitare Aggregatela concatenazione di stringhe, a causa del rischio che qualcuno non pensi all'algoritmo intrinseco di Schlemiel the Painter nell'uso ripetuto +. Invece, fallo e basta string.Join("", filePaths). Questo non avrà quel problema, anche per input molto grandi.
ErikE

3

Hai provato a derivare da ( StyleBundle o ScriptBundle ), non aggiungendo inclusioni nel tuo costruttore e quindi sovrascrivendo

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

Lo faccio per i fogli di stile dinamici e EnumerateFiles viene chiamato a ogni richiesta. Probabilmente non è la soluzione migliore ma funziona.


0

Mi scuso per rianimare un thread morto, tuttavia mi sono imbattuto in un problema simile con il caching del bundle in un sito Umbraco in cui volevo che i fogli di stile / script si minimizzassero automaticamente quando l'utente cambiava la bella versione nel backend.

Il codice che avevo già era (nel metodo onSaved per il foglio di stile):

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

e (onApplicationStarted):

BundleTable.EnableOptimizations = true;

Non importa cosa ho provato, il file "~ / bundles / styles.min.css" non sembra essere cambiato. Nella parte superiore della mia pagina, stavo originariamente caricando il foglio di stile in questo modo:

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

Tuttavia, l'ho fatto funzionare cambiando questo in:

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

Il metodo Styles.Render inserisce una stringa di query alla fine del nome del file che immagino sia la chiave della cache descritta da Hao sopra.

Per me è stato così semplice. Spero che questo aiuti chiunque altro come me che ha cercato su Google per ore e ha potuto trovare solo post di diversi anni!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.