So erzwingen Sie, dass BundleCollection zwischengespeicherte Skriptpakete in MVC4 leert

85

... oder wie ich gelernt habe, mich nicht mehr zu sorgen und einfach Code gegen vollständig undokumentierte APIs von Microsoft zu schreiben . Gibt es eine tatsächliche Dokumentation der offiziellen System.Web.OptimizationVeröffentlichung? Weil ich sicher keine finden kann, es keine XML-Dokumente gibt und alle Blog-Beiträge auf die RC-API verweisen, die sich wesentlich unterscheidet. Wie auch immer ..

Ich schreibe Code, um Javascript-Abhängigkeiten automatisch aufzulösen, und erstelle aus diesen Abhängigkeiten im laufenden Betrieb Bundles. Alles funktioniert hervorragend, außer wenn Sie Skripte bearbeiten oder auf andere Weise Änderungen vornehmen, die sich auf ein Bundle auswirken würden, ohne die Anwendung neu zu starten, werden die Änderungen nicht übernommen. Daher habe ich eine Option hinzugefügt, um das Caching der Abhängigkeiten für die Verwendung in der Entwicklung zu deaktivieren.

Anscheinend wird BundleTablesdie URL jedoch auch dann zwischengespeichert , wenn sich die Bundle-Sammlung geändert hat . Wenn ich beispielsweise in meinem eigenen Code ein Bundle neu erstellen möchte, gehe ich folgendermaßen vor:

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

Immer wenn ich ein Bundle mit demselben Alias entferne und neu erstelle , passiert absolut nichts: Die bundleUrlRückgabe von ResolveBundleUrlist dieselbe wie vor dem Entfernen und Neuerstellen des Bundles. Mit "dasselbe" meine ich, dass der Inhalts-Hash unverändert bleibt, um den neuen Inhalt des Bundles widerzuspiegeln.

bearbeiten ... eigentlich ist es viel schlimmer als das. Das Bundle selbst wird irgendwie außerhalb der BundlesSammlung zwischengespeichert. Wenn ich nur meinen eigenen zufälligen Hash generiere, um zu verhindern, dass der Browser das Skript zwischenspeichert, gibt ASP.NET das alte Skript zurück . Offensichtlich BundleTable.Bundlesbewirkt das Entfernen eines Bundles aus nichts.

Ich kann einfach den Alias ​​ändern, um dieses Problem zu umgehen, und das ist für die Entwicklung in Ordnung, aber ich mag diese Idee nicht, da ich entweder nach jedem Laden der Seite Aliase verwerfen muss oder eine BundleCollection habe, deren Größe zunimmt jede Seite wird geladen. Wenn Sie dies in einer Produktionsumgebung aktiviert lassen, wäre dies eine Katastrophe.

Es scheint also, dass ein Skript, wenn es bereitgestellt wird, unabhängig vom tatsächlichen BundleTables.BundlesObjekt zwischengespeichert wird. Wenn Sie also eine URL wiederverwenden, selbst wenn Sie das Bundle, auf das sie verwiesen hat, vor der Wiederverwendung entfernt haben, antwortet sie mit dem, was sich in ihrem Cache befindet, und das Ändern des BundlesObjekts leert den Cache nicht - also nur neue Elemente (oder Vielmehr würden jemals neue Gegenstände mit einem anderen Namen verwendet.

Das Verhalten scheint seltsam ... Wenn Sie etwas aus der Sammlung entfernen, sollte es aus dem Cache entfernt werden. Aber das tut es nicht. Es muss eine Möglichkeit geben, diesen Cache zu leeren und den aktuellen Inhalt des Caches BundleCollectionanstelle dessen zu verwenden, was beim ersten Zugriff auf dieses Bundle zwischengespeichert wurde.

Irgendeine Idee, wie ich das machen würde?

Es gibt diese ResetAllMethode, die einen unbekannten Zweck hat, aber trotzdem die Dinge kaputt macht, so dass es nicht so ist.

Jamie Treworgy
quelle
Selbes Problem hier. Ich glaube, ich habe es geschafft, meine zu lösen. Versuchen Sie und schauen Sie, ob es für Sie funktioniert. Stimme voll und ganz zu. Die Dokumentation für System.Web.Optimization ist Müll und alle Beispiele, die Sie im Internet finden, sind veraltet.
LeftyX
2
+1 für eine gute Referenz oben, kombiniert mit einem beißenden Kommentar über die Vertrauenserwartung von MS. Und auch für die Frage, auf die ich eine Antwort haben möchte.
Raif

Antworten:

33

Wir hören, dass Sie Probleme mit der Dokumentation haben. Leider ändert sich diese Funktion immer noch recht schnell, und das Generieren von Dokumentation hat einige Verzögerungen und kann fast sofort veraltet sein. Ricks Blog-Post ist aktuell und ich habe versucht, auch hier Fragen zu beantworten, um in der Zwischenzeit aktuelle Informationen zu verbreiten. Wir sind derzeit dabei, unsere offizielle Codeplex-Site einzurichten, die immer über die aktuelle Dokumentation verfügt.

Nun zu Ihrer speziellen Frage, wie Bundles aus dem Cache geleert werden sollen.

  1. Wir speichern die gebündelte Antwort im ASP.NET-Cache unter Verwendung eines Schlüssels, der aus der angeforderten Bundle-URL generiert wurde, dh Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"]wir richten auch Cache-Abhängigkeiten für alle Dateien und Verzeichnisse ein, die zum Generieren dieses Bundles verwendet wurden. Wenn sich also eine der zugrunde liegenden Dateien oder Verzeichnisse ändert, wird der Cache-Eintrag gelöscht.

  2. Wir unterstützen die Live-Aktualisierung der BundleTable / BundleCollection auf Anfrage nicht wirklich. Das vollständig unterstützte Szenario besteht darin, dass Bundles während des App-Starts konfiguriert werden (dies bedeutet, dass im Webfarm-Szenario alles ordnungsgemäß funktioniert, andernfalls würden einige Bundle-Anforderungen 404 sein, wenn sie an den falschen Server gesendet werden). Wenn Sie sich Ihr Codebeispiel ansehen, ist meine Vermutung, dass Sie versuchen, die Bundle-Sammlung bei einer bestimmten Anforderung dynamisch zu ändern? Jede Art von Bundle-Administration / Rekonfiguration sollte von einem Zurücksetzen der App-Domäne begleitet werden, um sicherzustellen, dass alles korrekt eingerichtet wurde.

Vermeiden Sie es daher, Ihre Bundle-Definitionen zu ändern, ohne Ihre App-Domain zu recyceln. Es steht Ihnen frei, die tatsächlichen Dateien in Ihren Bundles zu ändern, die automatisch erkannt werden sollen, und neue Hashcodes für Ihre Bundle-URLs zu generieren.

Hao Kung
quelle
2
Vielen Dank, dass Sie Ihr direktes Wissen hier einbringen! Ja - Ich versuche, die Bundle-Sammlung dynamisch zu ändern. Die Bundles basieren auf einer Reihe von Abhängigkeiten, die in einem anderen Skript beschrieben sind (das heißt, sie sind nicht unbedingt Teil des Bundles). Deshalb habe ich dieses Problem. Da das Ändern eines Skripts in einem Bundle einen Flush erzwingt, ist dies möglich. Gibt es eine Möglichkeit, eine manuelle Flush-Methode hinzuzufügen? Dies ist nicht entscheidend - dies dient der Bequemlichkeit während der Entwicklung -, aber ich hasse es, Code zu erstellen, der Probleme verursachen kann, wenn er versehentlich auf dem Produkt verwendet wird.
Jamie Treworgy
Können Sie auch das Problem der Webfarm näher erläutern? Würde das Hinzufügen eines neuen Bundles nach dem Start der Anwendung dazu führen, dass es nur auf dem Server verfügbar ist, auf dem es erstellt wurde - oder nur versucht, ein vorhandenes zu ändern? Dies wäre ein Dealkiller für das, was ich versuche, da es eine Laufzeitauflösung von Abhängigkeiten erfordert.
Jamie Treworgy
Sicher, wir könnten eine explizite Cache-Flush-äquivalente Methode hinzufügen, die intern bereits vorhanden ist. Stellen Sie sich in Bezug auf das Problem mit der Webfarm vor, Sie haben zwei Webserver A und B, Ihre Anfrage geht an A, der das Bundle hinzufügt, und sendet die Antwort. Ihr Client ruft jetzt den Inhalt des Bundles ab, aber die Anfrage geht an Server B, der das Bundle nicht registriert hat, und es gibt Ihre 404.
Hao Kung
1
Das Cache-Update ist verzögert. Wenn das Bundle zum ersten Mal verwendet wird (normalerweise durch Rendern eines Verweises auf das Bundle), wird es dem Cache hinzugefügt. Wenn Sie einen entsprechenden App-Start-Hook haben, mit dem Sie Ihre Bundles auf allen Webservern einrichten, bevor Sie mit der Bearbeitung von Anforderungen beginnen, sollte dies in Ordnung sein.
Hao Kung
2
Soweit ich das beurteilen kann, funktioniert das nicht. Das heißt, wenn ich die Bestandteile ändere, wird der Server-Cache nicht wie hier angegeben geleert. Sie müssen das Ding recyceln, um Änderungen zu erhalten. Weiß jemand, wo sich diese offizielle Dokumentation tatsächlich befindet?
Philw
21

Ich habe ein ähnliches Problem.
In meiner Klasse habe BundleConfigich versucht zu sehen, wie sich die Verwendung auswirkt BundleTable.EnableOptimizations = true.

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

        bundles.Add(...);
    }
}

Alles hat gut funktioniert.
Irgendwann habe ich ein Debugging durchgeführt und die Eigenschaft auf false gesetzt.
Ich hatte Mühe zu verstehen, was geschah, weil es den Anschein hatte, als würde das Bundle für jquery (das erste) nicht aufgelöst und geladen ( /bundles/jquery?v=).

Nach einigem Fluchen denke ich (?!) Habe ich es geschafft, die Dinge zu klären. Versuchen Sie hinzuzufügen bundles.Clear()und bundles.ResetAll()zu Beginn der Registrierung sollten die Dinge wieder funktionieren.

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

        BundleTable.EnableOptimizations = false;

        bundles.Add(...);
    }
}

Ich habe festgestellt, dass ich diese beiden Methoden nur ausführen muss, wenn ich die EnableOptimizationsEigenschaft ändere .

AKTUALISIEREN:

Wenn ich tiefer grabe, habe ich das herausgefunden BundleTable.Bundles.ResolveBundleUrlund habe @Scripts.Urlanscheinend Probleme, den Bundle-Pfad zu lösen.

Der Einfachheit halber habe ich einige Bilder hinzugefügt:

Bild 1

Ich habe die Optimierung deaktiviert und einige Skripte gebündelt.

Bild 2

Das gleiche Bündel ist im Körper enthalten.

Bild 3

@Scripts.Urlgibt mir den "optimierten" Pfad des Bündels, während @Scripts.Renderder richtige generiert wird.
Das gleiche passiert mit BundleTable.Bundles.ResolveBundleUrl.

Ich verwende Visual Studio 2010 + MVC 4 + Framework .Net 4.0.

LeftyX
quelle
Hmm ... die Sache ist, dass ich die Bundle-Tabelle eigentlich nicht löschen möchte, da sie viele andere von verschiedenen Seiten enthält (die aus verschiedenen Abhängigkeiten erstellt wurden). Da dies jedoch nur für die Arbeit in einer Entwicklungsumgebung gedacht ist, könnte ich den Inhalt kopieren, dann löschen und erneut hinzufügen, wenn dies den Cache leeren würde. Schrecklich ineffizient, aber wenn es funktioniert, ist es gut genug für Entwickler.
Jamie Treworgy
Stimmen Sie zu, aber das ist die einzige Option, die ich hatte. Ich habe den ganzen Nachmittag damit verbracht zu verstehen, was das Problem war.
LeftyX
2
Ich habe es gerade versucht, immer noch nicht den Cache leeren !! Ich lösche es ResetAllund habe versucht EnableOptimizations, sowohl beim Start als auch inline auf false zu setzen, wenn ich den Cache zurücksetzen muss. Es passiert nichts. Argh.
Jamie Treworgy
Es wäre sicher schön, wenn der Entwickler einen kurzen Blog-Beitrag mit sogar einem
Einzeiler
6
Um nur zu erklären, was diese Methoden tun: Scripts.Url ist nur ein Alias ​​für BundleTable.Bundles.ResolveBundleUrl. Es löst auch Nicht-Bundle-URLs auf, sodass es sich um einen generischen URL-Resolver handelt, der zufällig über Bundles Bescheid weiß. Scripts.Render verwendet das Flag EnableOptimizations, um zu bestimmen, ob ein Verweis auf Bundles oder die Komponenten, aus denen das Bundle besteht, gerendert werden soll.
Hao Kung
8

In Anbetracht der Empfehlungen von Hao Kung, dies aufgrund von Webfarmszenarien nicht zu tun, gibt es meines Erachtens viele Szenarien, in denen Sie dies möglicherweise tun möchten. Hier ist eine Lösung:

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

Sie können den obigen Code jederzeit aufrufen und Ihre Bundles werden aktualisiert. Dies funktioniert sowohl, wenn EnableOptimizations wahr oder falsch ist - mit anderen Worten, dies wirft das richtige Markup in Debug- oder Live-Szenarien aus, mit:

@Scripts.Render("~/bundles/your-bundle-virtual-path")
Zac
quelle
Lesen Sie hier weiter, was ein wenig über das Caching undGenerateBundleResponse
Zac
4

Ich hatte auch Probleme beim Aktualisieren von Bundles ohne Neuerstellung. Hier sind die wichtigen Dinge zu verstehen:

  • Das Bundle wird NICHT aktualisiert, wenn sich die Dateipfade ändern.
  • Das Bundle wird aktualisiert, wenn sich der virtuelle Pfad des Bundles ändert.
  • Das Bundle wird aktualisiert, wenn sich die Dateien auf der Festplatte ändern.

Wenn Sie also wissen, dass Sie beim dynamischen Bündeln Code schreiben können, damit der virtuelle Pfad des Bundles auf den Dateipfaden basiert. Ich empfehle, die Dateipfade zu hashen und diesen Hash an das Ende des virtuellen Pfads des Bundles anzuhängen. Auf diese Weise werden der virtuelle Pfad und das Bundle aktualisiert, wenn sich die Dateipfade ändern.

Hier ist der Code, mit dem ich das Problem gelöst habe:

    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
quelle
Ich empfehle, die AggregateVerkettung von Zeichenfolgen generell zu vermeiden , da das Risiko besteht, dass jemand bei wiederholter Verwendung nicht an den inhärenten Algorithmus von Schlemiel the Painter denkt +. Stattdessen einfach tun string.Join("", filePaths). Dies wird dieses Problem auch bei sehr großen Eingängen nicht haben.
ErikE
3

Haben Sie versucht, von ( StyleBundle oder ScriptBundle ) abzuleiten , Ihrem Konstruktor keine Einschlüsse hinzuzufügen und dann zu überschreiben ?

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

Ich mache das für dynamische Stylesheets und EnumerateFiles wird bei jeder Anfrage aufgerufen. Es ist wahrscheinlich nicht die beste Lösung, aber es funktioniert.

tulde23
quelle
0

Ich entschuldige mich für die Wiederbelebung eines toten Threads. Bei einer Umbraco-Site stieß ich jedoch auf ein ähnliches Problem mit dem Bundle-Caching, bei dem die Stylesheets / Skripte automatisch minimiert werden sollten, wenn der Benutzer die hübsche Version im Backend änderte.

Der Code, den ich bereits hatte, war (in der onSaved-Methode für das Stylesheet):

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

und (onApplicationStarted):

BundleTable.EnableOptimizations = true;

Egal was ich versuchte, die Datei "~ / bundles / styles.min.css" schien sich nicht zu ändern. Im Kopf meiner Seite habe ich ursprünglich wie folgt in das Stylesheet geladen:

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

Ich habe es jedoch zum Laufen gebracht, indem ich dies geändert habe in:

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

Die Styles.Render-Methode zieht eine Abfragezeichenfolge am Ende des Dateinamens ein, von der ich vermute, dass es sich um den von Hao oben beschriebenen Cache-Schlüssel handelt.

Für mich war es so einfach. Hoffe das hilft allen anderen wie mir, die dies stundenlang gegoogelt haben und nur einige Jahre alte Beiträge finden konnten!

SY6Dave
quelle