Cómo forzar a BundleCollection a vaciar paquetes de scripts en caché en MVC4

85

... o cómo aprendí a dejar de preocuparme y simplemente escribir código contra API completamente indocumentadas de Microsoft . ¿Existe alguna documentación real del System.Web.Optimizationlanzamiento oficial ? Porque seguro que no puedo encontrar ninguno, no hay documentos XML, y todas las publicaciones del blog se refieren a la API RC, que es sustancialmente diferente. Anyhoo ..

Estoy escribiendo un código para resolver automáticamente las dependencias de JavaScript y estoy creando paquetes sobre la marcha a partir de esas dependencias. Todo funciona muy bien, excepto si edita scripts o realiza cambios que afectarían a un paquete sin reiniciar la aplicación, los cambios no se reflejarán. Así que agregué una opción para deshabilitar el almacenamiento en caché de las dependencias para su uso en desarrollo.

Sin embargo, aparentemente BundleTablesalmacena en caché la URL incluso si la colección de paquetes ha cambiado . Por ejemplo, en mi propio código cuando quiero volver a crear un paquete, hago algo como esto:

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

Siempre que elimino y vuelvo a crear un paquete con el mismo alias , no pasa absolutamente nada: el bundleUrldevuelto ResolveBundleUrles el mismo que antes de eliminar y volver a crear el paquete. Por "lo mismo" me refiero a que el hash de contenido no se modifica para reflejar el nuevo contenido del paquete.

editar ... en realidad, es mucho peor que eso. El paquete en sí se almacena en caché de alguna manera fuera de la Bundlescolección. Si solo genero mi propio hash aleatorio para evitar que el navegador almacene en caché el script, ASP.NET devuelve el antiguo script . Entonces, aparentemente, quitar un paquete BundleTable.Bundlesno hace nada.

Simplemente puedo cambiar el alias para solucionar este problema, y ​​eso está bien para el desarrollo, pero no me gusta esa idea, ya que significa que tengo que desaprobar los alias después de cada carga de página o tener un BundleCollection que crece en tamaño en cada carga de página. Si deja esto en un entorno de producción, sería un desastre.

Entonces, parece que cuando se sirve un script, se almacena en caché independientemente del BundleTables.Bundlesobjeto real . Entonces, si reutiliza una URL, incluso si ha eliminado el paquete al que se refería antes de reutilizarlo, responde con lo que sea que esté en su caché, y la alteración del Bundlesobjeto no vacía el caché, por lo que solo los elementos nuevos (o más bien, se usarían elementos nuevos con un nombre diferente).

El comportamiento parece extraño ... eliminar algo de la colección debería eliminarlo del caché. Pero no es así. Debe haber una forma de vaciar esta caché y hacer que use el contenido actual del en BundleCollectionlugar de lo que almacenó en caché cuando se accedió por primera vez a ese paquete.

¿Alguna idea de cómo haría esto?

Existe este ResetAllmétodo que tiene un propósito desconocido pero simplemente rompe cosas de todos modos, así que no es así.

Jamie Treworgy
fuente
El mismo problema aqui. Creo que he logrado resolver el mío. Prueba y echa un vistazo si te funciona. Totalmente de acuerdo. La documentación para System.Web.Optimization es basura y todas las muestras que puede encontrar en Internet están desactualizadas.
LeftyX
2
+1 para una gran referencia en la parte superior combinado con un comentario mordaz sobre las expectativas de confianza de MS. Y también por hacer la pregunta a la que quiero una respuesta.
Raif

Respuestas:

33

Escuchamos su dolor con la documentación, desafortunadamente, esta característica todavía está cambiando bastante rápido y la generación de documentación tiene cierto retraso y puede quedar desactualizada casi de inmediato. Publicación del blog de Rick está actualizada y, mientras tanto, he intentado responder preguntas aquí para difundir información actual. Actualmente estamos en el proceso de configurar nuestro sitio oficial del Codeplex que siempre tendrá documentación actualizada.

Ahora en lo que respecta a su problema específico de cómo vaciar los paquetes del caché.

  1. Almacenamos la respuesta incluida dentro de la caché de ASP.NET utilizando una clave generada a partir de la URL del paquete solicitada, es decir Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"], también configuramos las dependencias de la caché contra todos los archivos y directorios que se utilizaron para generar este paquete. Entonces, si cambia alguno de los archivos o directorios subyacentes, la entrada de la caché se eliminará.

  2. Realmente no admitimos la actualización en vivo de BundleTable / BundleCollection por solicitud. El escenario totalmente compatible es que los paquetes se configuran durante el inicio de la aplicación (esto es para que todo funcione correctamente en el escenario de la granja web; de lo contrario, algunas solicitudes de paquetes terminarían siendo 404 si se envían al servidor incorrecto). Mirando su ejemplo de código, supongo que está intentando modificar la colección de paquetes de forma dinámica en una solicitud en particular. Cualquier tipo de administración / reconfiguración del paquete debe ir acompañado de un reinicio del dominio de la aplicación para garantizar que todo se haya configurado correctamente.

Por lo tanto, evite modificar las definiciones de su paquete sin reciclar el dominio de su aplicación. Puede modificar los archivos reales dentro de sus paquetes, que deberían detectarse automáticamente y generar nuevos códigos hash para las URL de sus paquetes.

Hao Kung
fuente
2
¡Gracias por traer su conocimiento directo aquí! Sí, estoy intentando modificar la colección de paquetes de forma dinámica. Los paquetes se crean en función de un conjunto de dependencias descritas en otro script (es decir, en sí mismo, no necesariamente parte del paquete), por lo que tengo este problema. Dado que cambiar un script que está en un paquete forzará una descarga, se puede hacer, ¿existe la posibilidad de agregar un método de descarga manual? Esto no es crucial, es por conveniencia durante el desarrollo, pero odio crear código que podría causar problemas si se usa accidentalmente en prod.
Jamie Treworgy
¿También puede ampliar el tema de la granja web? ¿Agregar un nuevo paquete después del inicio de la aplicación resultaría en que solo esté disponible en el servidor en el que se creó, o simplemente tratando de cambiar uno existente? Esto sería un poco problemático para lo que estoy tratando de hacer, ya que necesita resolver las dependencias en tiempo de ejecución.
Jamie Treworgy
Claro, podríamos agregar un método equivalente de vaciado de caché explícito, ya está allí internamente. Con respecto al problema de la granja web, básicamente imagina que tienes dos servidores web A y B, tu solicitud va a A, quien agrega el paquete y envía la respuesta, tu cliente ahora va a buscar el contenido del paquete, pero la solicitud va a servidor B que no registró el paquete, y ahí está su 404.
Hao Kung
1
La actualización de la caché es lenta, la primera vez que se usa el paquete (normalmente mediante la representación de una referencia al paquete), se agrega a la caché. Si tiene un enlace de inicio de aplicación equivalente en el que configura sus paquetes en todos los servidores web antes de comenzar a manejar las solicitudes, debería estar bien.
Hao Kung
2
Por lo que puedo decir, esto no funciona. Es decir, si cambio los archivos constituyentes, la caché del servidor no se borra como se indica aquí. Tienes que reciclar la cosa para que haya cambios. ¿Alguien sabe dónde está realmente esa documentación oficial?
philw
21

Tengo un problema similar.
En mi clase BundleConfigestaba tratando de ver cuál era el efecto de consumir BundleTable.EnableOptimizations = true.

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

        bundles.Add(...);
    }
}

Todo estaba funcionando bien.
En algún momento, estaba haciendo una depuración y establecí la propiedad en falso.
Me costó entender lo que estaba sucediendo porque parecía que el paquete de jquery (el primero) no se resolvería y cargaría ( /bundles/jquery?v=).

Después de algunas palabrotas, creo (?!) Que me las arreglé para arreglar las cosas. Intente agregar bundles.Clear()y bundles.ResetAll()al comienzo del registro y las cosas deberían comenzar a funcionar nuevamente.

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

        BundleTable.EnableOptimizations = false;

        bundles.Add(...);
    }
}

Me di cuenta de que necesito ejecutar estos dos métodos solo cuando cambio la EnableOptimizationspropiedad.

ACTUALIZAR:

Profundizando, lo descubrí BundleTable.Bundles.ResolveBundleUrly @Scripts.Urlparece que tengo problemas para resolver la ruta del paquete.

En aras de la simplicidad, agregué algunas imágenes:

imagen 1

He desactivado la optimización y he incluido algunos scripts.

imagen 2

El mismo paquete está incluido en el cuerpo.

imagen 3

@Scripts.Urlme da la ruta "optimizada" del paquete mientras @Scripts.Rendergenera la correcta.
Lo mismo pasa con BundleTable.Bundles.ResolveBundleUrl.

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

LeftyX
fuente
Hmm ... la cosa es que en realidad no quiero borrar la tabla de paquetes, porque contendrá muchas otras de diferentes páginas (creadas a partir de diferentes conjuntos de dependencias). Pero dado que esto es realmente solo para trabajar en un entorno de desarrollo, creo que podría copiar el contenido, luego borrarlo y luego agregarlo nuevamente, si eso vaciaría el caché. Horriblemente ineficiente, pero si funciona, es lo suficientemente bueno para el desarrollador.
Jamie Treworgy
De acuerdo, pero esa es la única opción que tenía. Me he pasado toda la tarde tratando de entender cuál era el problema.
LeftyX
2
Lo acabo de intentar, ¡TODAVÍA sin vaciar el caché! Lo borré ResetAll, y he intentado configurarlo EnableOptimizationsen falso tanto al inicio como en línea cuando necesito restablecer el caché, no sucede nada. Argh.
Jamie Treworgy
Seguro que sería bueno si el desarrollador pudiera lanzar una publicación de blog rápida con una sola línea sobre los métodos en estos objetos :)
Jamie Treworgy
6
Entonces, solo para explicar lo que hacen estos métodos: Scripts.Url es solo un alias para BundleTable.Bundles.ResolveBundleUrl, también resolverá las URL que no son de paquete, por lo que es un solucionador de URL genérico que sabe sobre paquetes. Scripts.Render utiliza la marca EnableOptimizations para determinar si se debe representar una referencia a los paquetes o los componentes que componen el paquete.
Hao Kung
8

Teniendo en cuenta las recomendaciones de Hao Kung de no hacer esto debido a escenarios de granjas web, creo que hay muchos escenarios en los que es posible que desee hacer esto. He aquí una solución:

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

Puede llamar al código anterior en cualquier momento y sus paquetes se actualizarán. Esto funciona tanto cuando EnableOptimizations es verdadero o falso; en otras palabras, esto arrojará el marcado correcto en escenarios de depuración o en vivo, con:

@Scripts.Render("~/bundles/your-bundle-virtual-path")
Zac
fuente
Leer más aquí que habla un poco sobre el almacenamiento en caché yGenerateBundleResponse
Zac
4

También encontré problemas con la actualización de paquetes sin reconstruir. Estas son las cosas importantes que debe comprender:

  • El paquete NO se actualiza si cambian las rutas de archivo.
  • El paquete SÍ se actualiza si cambia la ruta virtual del paquete.
  • El paquete se actualiza si cambian los archivos en el disco.

Entonces, sabiendo eso, si está haciendo un paquete dinámico, puede escribir algún código para hacer que la ruta virtual del paquete se base en las rutas de los archivos. Recomiendo aplicar hash a las rutas de archivo y agregar ese hash al final de la ruta virtual del paquete. De esta manera, cuando cambian las rutas de archivo, también lo hace la ruta virtual y el paquete se actualizará.

Aquí está el código con el que terminé que me resolvió el problema:

    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
fuente
En general, recomiendo evitar la Aggregateconcatenación de cadenas, debido al riesgo de que alguien no piense en el algoritmo inherente de Schlemiel the Painter al usarlo repetidamente +. En su lugar, hazlo string.Join("", filePaths). Esto no tendrá ese problema, incluso para entradas muy grandes.
ErikE
3

¿Ha intentado derivar de ( StyleBundle o ScriptBundle ), sin agregar inclusiones en su constructor y luego anulando

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

Hago esto para hojas de estilo dinámicas y se llama a EnumerateFiles en cada solicitud. Probablemente no sea la mejor solución, pero funciona.

tulde23
fuente
0

Disculpas por revivir un hilo muerto, sin embargo, encontré un problema similar con el almacenamiento en caché de Bundle en un sitio de Umbraco donde quería que las hojas de estilo / scripts se minimizaran automáticamente cuando el usuario cambiaba la versión bonita en el backend.

El código que ya tenía era (en el método onSaved para la hoja de estilo):

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

y (onApplicationStarted):

BundleTable.EnableOptimizations = true;

No importa lo que intenté, el archivo "~ / bundles / styles.min.css" no pareció cambiar. En el encabezado de mi página, originalmente estaba cargando en la hoja de estilo así:

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

Sin embargo, lo hice funcionar cambiando esto a:

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

El método Styles.Render extrae una cadena de consulta al final del nombre del archivo, que supongo que es la clave de caché descrita por Hao anteriormente.

Para mí, fue tan simple como eso. ¡Espero que esto ayude a cualquier otra persona como yo que estuvo buscando en Google durante horas y solo pudo encontrar publicaciones de varios años!

SY6Dave
fuente