sábado, 2 de noviembre de 2013

Intríngulis de los bundles de ASP.NET MVC

Una de las características que mas me gusta de ASP.NET MVC es su capacidad de crear bundles de javascript y css. Lo encuentro muy útil y además es una buena práctica que ninguna aplicación web debería pasar por alto.

En este post no quiero mostrar como crearlos, para eso ya existe un excelente tutorial en el portal de asp.net, Bundling and Minification, además de artículos sobre temas concretos que no cubre el anterior tutorial, como el fallback si utilizas CDN

Sin embargo, después de un tiempo utilizándolos, me he dado cuenta que tienen algunos comportamientos, cuando menos peculiares, que quizás requieran una explicación. En concreto, me voy a centrar en 2 ideas:

  • Dar soporte a los ficheros map
  • El oculto significado del parámetro virtualPath

Dar soporte a los ficheros map

Una de las grandes ventajas de utilizar bundles en que nuestro código javascript será minificado (o minimizado, nunca me siento a gusto con esta palabra). Siendo así, el tamaño del fichero se verá reducido… y también su contenido se verá alterado. Es decir, la minificación de los bundles de ASP.NET MVC, además de eliminar retornos de carro, comentarios, espacios, la palabra debugger, etc. también “ofusca” el código (aquí no me quiero meter en ningún jardín, pero yo creo que renombrar el nombre de la variable “saludo” al nombre “n” es ofuscar… digo yo) y por sí fuera poco también durante la minificación se puede tomar ciertas libertades y “optimizar” algunos bloques de código. Cuidado con esto último porque no sería la primera vez que la versión minificada no funciona correctamente porque al señor minificador se le fue la mano!

En cualquier caso, está claro que nuestro fichero .js original no es el mismo fichero .js que es servido al cliente. De este modo, cuando queramos depurar en cliente nuestro código javascript, no va a ser una tarea fácil. Pues bien, los ficheros .map vienen al rescate (un montón de info relacionada con la especificación la puedes encontrar aquí).

Si tenemos instalada la extensión Web Essentials en Visual Studio (y asumo que casi cualquier programador web la tendrá instalada), es muy fácil minificar un fichero. Simplemente click derecho y “Minify JavasScript file(s)”. Si nos fijamos, además de crear nuestro fichero .min.js, también creará un fichero .min.js.map

Por ejemplo, para el siguiente código en el fichero main.js

function holaMundo() {

    // Saludar

    var saludo = "Hola mundo!";

    alert(saludo);

}

Generará un fichero main.min.js y un fichero main.min.js.map

El fichero main.min.js

function holaMundo(){alert("Hola mundo!")}

//# sourceMappingURL=main.min.js.map

y el fichero main.min.js.map

{

"version":3,

"file":"main.min.js",

"lineCount":1,

"mappings":"AAAAA,SAASA,SAAS,CAAA,CAAG,CAMjBC,KAAK,CAFQ,aAER,CANY",

"sources":["main.js"],

"names":["holaMundo","alert"]

}

El caso es que el fichero .map nos servirá para poder “desminificar/desofucar” nuestro fichero .min.js en cliente.

Yo esto sólo lo he probado con Chrome pero entiendo que tarde o temprano lo soportarán todos los navegadores. Con Chrome, si pedimos la página sólo descargará el fichero .min.js (el que hemos cargado con la etiqueta <script>). Sin embargo, si tenemos previamente abiertas las Dev Tools (ojo que no vale abrirlas una vez ya estamos en la página), veremos que además de descargarse el fichero .min.js también se descargará el fichero .min.js.map (sólo si tenemos abiertas las Dev Tools, claro, sino sería un derroche).

image

image

Será cuando seleccionemos el fichero original (el no minificado) en la pestaña Sources cuando se descargará realmente el fichero main.js (el original) para poder comenzar a depurar o ver su contenido ¡mola!

image

Chrome por defecto carga los ficheros .map, esto se puede configurar desde las opciones del navegador.

image

Respecto a las opciones de Chrome, sólo mencionar que también existe el concepto de fichero .map para css. Por ejemplo Bootstrap en sus últimas versiones ya lo incorpora y no te asustes si ves peticiones a ficheros .less que “no” tienes en tu proyecto, porque son justamente parte del fichero .map y de la clave sourcesContent.

image

Hasta aquí todo genial… pero hasta aquí nada hemos hablado todavía de los bundles de ASP.NET MVC. Por ahora, sólo hemos utilizado Web Essentials y hemos servidor directamente el fichero .min.js.

Cuando metemos a los bundles en la ecuación… pues todo este escenario se va “tristemente” al traste. Esto es porque los bundles, lejos de detectar que ya existe un fichero .min.js y dejarlo intacto, lo detectan y ya puestos le meten otra “pasadita” al minificador, con lo que el comentario de Web Essentials que da soporte a los ficheros .map desaparece :( Es decir, nuestro anterior fichero .min.js pasa a tener el siguiente código:

function holaMundo(){alert("Hola mundo!")}

Siendo así, bundles y ficheros .map parecen un amor imposible.

¿La solución? Pues quizás hacerse un minificador personalizado (implementando IBundleTransform, eso en otro post…) o pasar de los bundles y utilizar sólo Web Essentials (que por cierto también da soporte a bundles, aunque de forma manual). Un ejemplo de bundles con Web Essentials se puede encontrar en este post Minify resources with source map at runtime using Web Essential

El oculto significado del parámetro virtualPath

El parámetro virtualPath es ese pequeño y aparentemente insignificante valor que se le pasa al constructor de ScriptBundle y StyleBundle. ¡Pon lo que quieras, dicen! ¡Es sólo un nombre!… pues no, amigos, ¡es más que un nombre! :)

Ya sabes que por defecto una aplicación ASP.NET MVC te crea algunos bundles, pues vamos a trabajar con el que carga los estilos del sitio (site.css) para ver primero el error y explicar después el porqué.

Por defecto, trae lo siguiente:

bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));

El único cambio que vamos a hacer es cambiar el valor del parámetro virtualPath a ~/Content (sin /css).

bundles.Add(new StyleBundle("~/Content").Include("~/Content/site.css"));

Y al pedir la página (siempre que estemos en release o con BundleTable.EnableOptimizations = true;… vamos, que se estén haciendo los bundles), tenemos un bonito error 403 Forbidden

image

Como no sabía por donde pillar esto, me he bajado la trial de Reflector (¡pedazo de software!) y he depurado la clase BundleHandler del ensamblado System.Web.Optimization. En concreto, en el método RemapHandlerForBundleRequests está escrito el siguiente código:

if (!virtualPathProvider.FileExists(appRelativeCurrentExecutionFilePath)

    && !virtualPathProvider.DirectoryExists(appRelativeCurrentExecutionFilePath))

{

    //devolver bundle

    return true;

}

return false;

Es decir, si virtualPath existe en disco NO se devolverá ningún bundle. En nuestro caso la carpeta ~/Content existe, así que !adiós bundle!

Con ScriptBundle no suele haber problemas porque por imitación al código de la plantilla del proyecto, llamamos a los bundles siempre “~/bundles/<algo>” (y la carpeta bundles no existe). Sin embargo con StyleBundle (y también por imitación) lo llamamos ~/Content/<algo>… y aquí sí suele haber problemas.

A mí esto me ha dado más un dolor de cabeza, sobre todo porque en desarrollo no pasa (no se están haciendo bundles) y luego en producción o durante las pruebas es cuando te salta la liebre.

Otro problema que nos puede acarrear virtualPath es que las rutas a imágenes de las hojas de estilo serán relativas al nombre seleccionado. Esto no es un problema en desarrollo (puesto que el bundle no se realiza) pero sí en producción.

Dicho esto, me siguen gustando los bundles, pero sabiendo lidiar con estos pequeños intríngulis.

Un saludo!

No hay comentarios:

Publicar un comentario