lunes, 15 de marzo de 2010

VS 2010: Crear versiones optimizadas (minified) de archivos javascript con T4 text templates.

Una de las vetas más productivas que encontramos al utilizar los Text Templates en Visual Studio 2010 tiene que ver con la posibilidad de acceder al modelo de objetos de Visual Studio (EnvDte).

Este modelo de objetos nos permite analizar e incluso modificar no sólo la estructura de archivos del proyecto que estamos construyendo (archivos y proyectos incluidos, referencias, propiedades…) sino también del código (namespaces, clases, propiedades, métodos, variables…).

Cierto es que es algo que ya podíamos hacer programando un plug-in o un diseñador, pero estas opciones conllevan cierta complejidad. Veremos, a través de este ejemplo, que el uso de T4 para las tareas que antes requerían ese tipo de soluciones simplifica mucho las cosas, si bien el resultado es un poco menos… elegante (que un diseñador, por ejemplo).

El ejemplo consiste en crear una plantilla T4 que recorra los archivos javascript de un proyecto y cree y agregue versiones optimizadas de ellos utilizando la librería Microsoft Ajax Minifier. Empecemos.

Crear una plantilla preprocesada.

Como ésta es una plantilla que podríamos reutilizar en varios proyectos, vamos a crear una preprocesada (Preprocessed Text Template) y ubicarla en una librería separada que luego podamos distribuir.

Así que comenzamos creando una solución de prueba (“T4Sample”), agregando un proyecto de tipo Class Library (“JavascriptCruncher”) y un proyecto de tipo Web Application (“SampleWebApplication”). En el proyecto de librería insertamos un nuevo ítem de tipo “Preprocessed Text Template” (“JavascriptCruncherTemplate”) y una clase en la que ubicaremos los métodos auxiliares que vayamos necesitando (“EnvDteHelper”). La solución debería quedar así:

JsCruncher01

Obtener una referencia a EnvDte.

El disparador de todo esto ha sido un excelente post de Oleg Sych, al que le vamos a pedir prestado el código para obtener la referencia al modelo de objetos de Visual Studio. Ubicaremos este código en nuestra clase auxiliar, como un método estático:

using System;
using System.Collections.Generic;
using System.IO;
using EnvDTE;
using Microsoft.VisualStudio.TextTemplating;

namespace JavascriptCruncher
{
    public class EnvDteHelper
    {
        public static Project GetProject(ITextTemplatingEngineHost host)
        {
            IServiceProvider hostServiceProvider = (IServiceProvider)host;
            if (hostServiceProvider == null)
                throw new Exception("Host property returned unexpected value (null).");

            DTE dte = (DTE)hostServiceProvider.GetService(typeof(DTE));
            if (dte == null)
                throw new Exception("Unable to retrieve EnvDTE.DTE");

            Array activeSolutionProjects = (Array)dte.ActiveSolutionProjects;
            if (activeSolutionProjects == null)
                throw new Exception("DTE.ActiveSolutionProjects returned null.");

            Project dteProject = (Project)activeSolutionProjects.GetValue(0);
            if (dteProject == null)
                throw new Exception("DTE.ActiveSolutionProjects returned null.");

            return dteProject;
        }
    }
}

Para que lo anterior funcione necesitamos referencias a las librerías “EnvDte” y “Microsoft.VisualStudio.TextTemplating.Interfaces.10.0”. Esta última (creo) no se incluye en la distribución del VS 2010. Si no la encuentran, pueden obtenerla bajándose el Visual Studio 2010 SDK.

Obtener todos los archivos javascript incluidos en el proyecto.

En el paso anterior obtuvimos una referencia al proyecto en donde corre el template. El próximo paso es obtener una lista de todos los objetos ProjectItem (una interfaz que representa cada archivo y carpeta en el proyecto) que corresponden a archivos javascript.

El proyecto está representado como una jerarquía de ProjectItems que debemos recorrer recursivamente. Necesitamos todos los archivos “.js”, pero excluyendo aquellos “.min.js”, entendiendo que éstos ya están optimizados (“minificados” podríamos decir maltraduciendo “minified”).

Agregamos entonces las siguientes funciones a nuestra clase EnvDteHelper.

public static List<projectitem> GetJsProjectItems(Project project)
{
    List<projectitem> jsProjectItems = new List<projectitem>();

    foreach (ProjectItem projectItem in project.ProjectItems)
    {
        GetJsProjectItems(projectItem, jsProjectItems);

        if (projectItem.Name.EndsWith(".js") && !projectItem.Name.EndsWith(".min.js"))
            jsProjectItems.Add(projectItem);
    }

    return jsProjectItems;
}

private static void GetJsProjectItems(ProjectItem parentProjectItem, List<projectitem> jsProjectItems)
{
    foreach (ProjectItem projectItem in parentProjectItem.ProjectItems)
    {
        GetJsProjectItems(projectItem, jsProjectItems);

        if (projectItem.Name.EndsWith(".js") && !projectItem.Name.EndsWith(".min.js"))                    
            jsProjectItems.Add(projectItem);                
    }
}
Optimizar o minificar los archivos.

Este es el momento (si no lo han hecho ya) de bajar e instalar el Microsoft Ajax Minifier y agregar una referencia a la librería ajaxmin.dll en el proyecto (JavascriptCruncher).

Ésta librería es muy simple. La clase ScriptCruncher contiene el método “Crunch”, que recibe el código javascript a optimizar (minificar) y una instancia de la clase CodeSettings con las opciones deseadas y devuelve el código minificado como una cadena.

Combinando lo visto en el punto anterior con esta librería, podemos comenzar a escribir el template propiamente dicho. Sería algo así (lo siguiente no es el template completo, sólo un ejemplo del avance hasta ahora):

EnvDTE.Project project = EnvDteHelper.GetProject(this.Host);
List<envdte.projectitem> jsProjectItems = EnvDteHelper.GetJsProjectItems(project);

ScriptCruncher cruncher = new ScriptCruncher();
CodeSettings crunchSettings = new CodeSettings();
crunchSettings.CollapseToLiteral = true;
crunchSettings.LocalRenaming = LocalRenaming.CrunchAll;	
crunchSettings.StripDebugStatements=true;

foreach( EnvDTE.ProjectItem item in jsProjectItems)
{	
	string itemFileName = item.FileNames[0]; 
	string jsCode = File.ReadAllText(itemFileName);
	string jsMinified = cruncher.Crunch(jsCode, crunchSettings);
	
	this.WriteLine(itemFileName);
	this.WriteLine(jsMinified);
	this.WriteLine("--------------------");	
}

El ejemplo anterior solamente muestra el código minificado. Pero tenemos que…

Crear un archivo con el código minificado y agregarlo al proyecto programáticamente.

Vamos a recurrir otra vez al bueno de Oleg Sych para ver cómo se lleva a cabo esta tarea. Mirando un poco sobre el código de su ejemplo llegamos a que agregar un archivo es tan simple como utilizar el método AddFromFile de la colección ProjectItems del objeto ProjectItem correspondiente al archivo javascript con el código original.

Como se ve, le pasamos el ProjectItem del archivo javascript original -que utilizará como referencia- y el código minificado. Lo único que hace es cambiar la extensión, grabar (o sobreescribir) el archivo minificado existente y agregarlo al proyecto (si es que no estaba ya agregado). Ubicamos este nuevo helper junto a los demás en EnvDteHelper

public static void SaveMinifiedCode(ProjectItem originalJsItem, string minifiedCode)
{
    string outputFileName = Path.ChangeExtension(originalJsItem.FileNames[0], ".min.js");
    File.WriteAllText(outputFileName, minifiedCode);

    ProjectItem parentProjectItem = originalJsItem.Properties.Parent;
    parentProjectItem.ProjectItems.AddFromFile(outputFileName);
}
Juntando las piezas en la plantilla.

Hasta ahora venimos trabajando sobre nuestra clase EnvDteHelper. Suelo recomendar esto ya que, por ahora, el soporte de Intellisense en las plantillas es bastante limitado. Es mucho más fácil, entonces, trabajar dentro de lo posible en estos helpers, que son clases comunes y silvestres, para luego juntar todo en el template en un último paso.

Una vez terminado el trabajo, la plantilla (“JavascriptCruncherTemplate.tt”) debe quedar así:

<#@ template language="C#" hostSpecific="true" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Microsoft.Ajax.Utilities" #>
<#@ import namespace="System.IO" #>
<# 
EnvDTE.Project project = EnvDteHelper.GetProject(this.Host);
List<EnvDTE.ProjectItem> jsProjectItems = EnvDteHelper.GetJsProjectItems(project);

ScriptCruncher cruncher = new ScriptCruncher();
CodeSettings crunchSettings = new CodeSettings();
crunchSettings.CollapseToLiteral = true;
crunchSettings.LocalRenaming = LocalRenaming.CrunchAll;	
crunchSettings.StripDebugStatements=true;

foreach( EnvDTE.ProjectItem item in jsProjectItems)
{	
	string itemFileName = item.FileNames[0]; 
	string jsCode = File.ReadAllText(itemFileName);
	string jsMinified = cruncher.Crunch(jsCode, crunchSettings);
	
	EnvDteHelper.SaveMinifiedCode(item, jsMinified);
	this.WriteLine( "Processed: {0}", itemFileName);
}
this.WriteLine("done!");
#>

Para destacar: noten, en el tag inicial de la plantilla, que se especifica la opción “hostSpecific=true”. Esto es importante ya que le indica al generador que la clase resultante debe tener la propiedad Host. Si no lo hacemos obtendremos un error de compilación luego (¡me costó un rato darme cuenta de eso!). Vean también la declaración de los “import” (el equivalente a “using” en los templates).

El resto del código es simple dadas las herramientas que nos construimos:

  • obtenemos la referencia al proyecto,
  • luego la lista de ProjectItems que corresponden a archivos javascript.
  • Inicializamos un objeto ScriptCruncher y otro CodeSettings. Especificamos algunas propiedades que hacen al método de minificación (¿inventé una palabra?).
  • Luego recorremos la lista de ProjectItems (archivos javascript) y los vamos minificando utilizando el “cruncher”).
  • Y grabamos cada uno de ellos utilizando el helper que creamos (“SaveMinifiedCode”).
  • Utilizamos el output del template en sí como un log, escribiendo el nombre de cada archivo que procesamos. Este output es informativo y no se compilará.
Utilizar la plantilla en un proyecto.

Nuestra librería JavascriptCruncher está lista para ser utilizada en un proyecto de prueba. ¿Cómo la referenciamos?

Lo que debemos hacer es crear una plantilla en cada proyecto en el que querramos utilizar nuestro JavascriptCruncherTemplate que haga referencia a éste.

Hice un proyecto de prueba muy tonto, que tiene dos páginas iguales, una en la carpeta principal y otro en una subcarpeta (para verificar que el template genere ambos archivos). También agregué, en la carpeta principal un elemento de tipo “Text Template” (JavascriptCruncher.tt) que hará referencia al template precompilado que acabamos de crear. La estructura, en resumen, queda así:

JsCruncher02

JavascriptCruncher.tt referencia al template con este código:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".log" #>
<#@ assembly name="JavascriptCruncher" #>
<#@ import namespace="JavascriptCruncher" #>
<#
    JavascriptCruncherTemplate t = new JavascriptCruncherTemplate();
	t.Host = this.Host;
    this.Write(t.TransformText());
#>

Noten que simplemente crea una instancia de JavascriptCruncherTemplate, establece la propiedad host (recuerden nuevamente que es importante la propiedad hostspecific="true" de la primera línea) e invoca al método TransformText.

Referencia a la librería de templates utilizando la GAC.

La línea en JavascriptCruncher.tt de nuestro proyecto de prueba que hace referencia a la librería de templates que acabamos de crear es la que dice:

<#@ assembly name="JavascriptCruncher" #>

Puedo hacerlo de esta manera -sin indicar el path a la dll- ya que previamente incluí a JavascriptCruncher.dll y ajaxmin.dll en la GAC.

Hay otras opciones, como indica Oleg Sych en Understanding T4: <#@ assembly #> directive, pero la realidad es que por uno u otro motivo no pude hacer funcionar ninguna de ellas limpiamente, así que yo recomiendo ésta de agregar la librería a la GAC, ustedes experimenten por su cuenta y después me dicen.

Para esto primero hay que firmar JavascriptCruncher.dll, lo que es muy sencillo. En propiedades del proyecto vamos a la solapa “Signing”. Indicamos que firme el ensamblado y elegimos un nombre de archivo, más una password (opcional):

JsCruncher03

Tenemos que compilar y luego utilizar gacutil para registrar las dos librerías (la otra es ajaxmin.dll que es referenciada por la nuestra, y que tampoco está en la gac). En la línea de comandos de Visual Studio 2010 (“Menú de Inicio / Todos los programas / Microsoft Visual Studio 2010 / Visual Studio Tools / Visual Studio Command Prompt (2010)”):

gacutil -i "[path completo]\JavascriptCruncher.dll"

y no nos olvidemos de ajaxmin.dll, que también deberá estar en la GAC:

gacutil -i "[path completo]\ajaxmin.dll"

Probando el template.

Si corremos el template (grabándolo o utilizando la opción “Run Custom Tool” del menú contextual que aparece al hacer click con el botón de la derecha sobre el template) veremos cómo se agregan los archivos javascript minificados:

JsCruncher04

Código fuente del ejemplo.

Pueden descargarse el código fuente de este ejemplo desde el repositorio de desdesarrollo en google code (no se olviden de registrar las librerías en la GAC).

No hay comentarios.: