Cómo compilar aplicaciones web con Yeoman y Polymer

Crea la estructura de tus apps web con herramientas modernas

Introducción

Allo’ Allo’. Cualquier persona que escriba una app web sabe lo importante que es mantener la productividad. Es un desafío cuando tienes que preocuparte por tareas tediosas, como encontrar el código estándar adecuado, configurar un flujo de trabajo de desarrollo y pruebas, y reducir y comprimir todas tus fuentes.

Afortunadamente, las herramientas modernas de frontend pueden ayudar a automatizar gran parte de esto, lo que te permite enfocarte en escribir una app increíble. En este artículo, se mostrará cómo usar Yeoman, un flujo de trabajo de herramientas para apps web que optimiza la creación de apps con Polymer, una biblioteca de polyfills y azúcar para desarrollar apps con componentes web.

Yeoman

Conoce a Yo, Grunt y Bower

Yeoman es un hombre con sombrero que tiene tres herramientas para mejorar tu productividad:

  • yo es una herramienta de scaffolding que ofrece un ecosistema de scaffolds específicos de frameworks, llamados generadores, que se pueden usar para realizar algunas de las tareas tediosas que mencioné antes.
  • grunt se usa para compilar, obtener una vista previa y probar tu proyecto, gracias a la ayuda de las tareas seleccionadas por el equipo de Yeoman y grunt-contrib.
  • bower se usa para administrar dependencias, de modo que ya no tengas que descargar y administrar tus secuencias de comandos de forma manual.

Con solo uno o dos comandos, Yeoman puede escribir código estándar para tu app (o partes individuales, como modelos), compilar tu Sass, minimizar y concatenar tus CSS, JS, HTML e imágenes, y activar un servidor web simple en tu directorio actual. También puede ejecutar tus pruebas de unidades y mucho más.

Puedes instalar generadores desde Node Packaged Modules (npm). Actualmente, hay más de 220 generadores disponibles, muchos de los cuales fueron escritos por la comunidad de código abierto. Entre los generadores populares, se incluyen generator-angular, generator-backbone y generator-ember.

Página principal de Yeoman

Con una versión reciente de Node.js instalada, ve a la terminal más cercana y ejecuta lo siguiente:

$ npm install -g yo

Eso es todo. Ahora tienes Yo, Grunt y Bower, y puedes ejecutarlos directamente desde la línea de comandos. Este es el resultado de ejecutar yo:

Instalación de Yeoman

Generador de Polymer

Como mencioné antes, Polymer es una biblioteca de polyfills y azúcar que permite el uso de componentes web en navegadores actualizados. El proyecto permite que los desarrolladores creen apps con la plataforma del futuro y que informen al W3C sobre los lugares en los que se pueden mejorar las especificaciones en curso.

Página principal del generador de Polymer

generator-polymer es un nuevo generador que te ayuda a estructurar apps de Polymer con Yeoman, lo que te permite crear y personalizar fácilmente elementos (personalizados) de Polymer a través de la línea de comandos y, luego, importarlos con importaciones de HTML. Esto te ahorra tiempo, ya que escribe el código estándar por ti.

A continuación, instala el generador de Polymer ejecutando lo siguiente:

$ npm install generator-polymer -g

Eso es todo. Ahora tu app tiene superpoderes de componentes web.

Nuestro generador recién instalado tiene algunos bits específicos a los que tendrás acceso:

  • polymer:element se usa para crear nuevos elementos individuales de Polymer. Por ejemplo: yo polymer:element carousel.
  • polymer:app se usa para estructurar tu index.html inicial, un Gruntfile.js que contiene la configuración del tiempo de compilación para tu proyecto, así como las tareas de Grunt y una estructura de carpetas recomendada para el proyecto. También te dará la opción de usar Sass Bootstrap para los estilos de tu proyecto.

Compila una app de Polymer

Crearemos un blog sencillo con algunos elementos personalizados de Polymer y nuestro nuevo generador.

App de Polymer

Para comenzar, ve a la terminal, crea un directorio nuevo y ábrelo con el comando cd usando mkdir my-new-project && cd $_. Ahora puedes iniciar tu app de Polymer ejecutando lo siguiente:

$ yo polymer
Compilación de apps de Polymer

Esto obtiene la versión más reciente de Polymer de Bower y genera un archivo index.html, una estructura de directorios y tareas de Grunt para tu flujo de trabajo. ¿Por qué no tomas un café mientras esperas a que la app termine de prepararse?

A continuación, podemos ejecutar grunt server para obtener una vista previa de la app:

Servidor de Grunt

El servidor admite LiveReload, lo que significa que puedes abrir un editor de texto, editar un elemento personalizado y el navegador se volverá a cargar cuando guardes los cambios. Esto te brinda una vista en tiempo real del estado actual de tu app.

A continuación, crearemos un nuevo elemento de Polymer para representar una entrada de blog.

$ yo polymer:element post
Crea un elemento de publicación

Yeoman nos hace algunas preguntas, como si nos gustaría incluir un constructor o usar una importación de HTML para incluir el elemento post en index.html. Por ahora, digamos No a las dos primeras opciones y dejemos la tercera en blanco.

$ yo polymer:element post

[?] Would you like to include constructor=''? No

[?] Import to your index.html using HTML imports? No

[?] Import other elements into this one? (e.g 'another_element.html' or leave blank)

    create app/elements/post.html

Esto crea un nuevo elemento de Polymer en el directorio /elements llamado post.html:

<polymer-element name="post-element"  attributes="">

    <template>

    <style>
        @host { :scope {display: block;} }
    </style>

    <span>I'm <b>post-element</b>. This is my Shadow DOM.</span>

    </template>

    <script>

    Polymer('post-element', {

        //applyAuthorStyles: true,

        //resetStyleInheritance: true,

        created: function() { },

        enteredView: function() { },

        leftView: function() { },

        attributeChanged: function(attrName, oldVal, newVal) { }

    });

    </script>

</polymer-element>

Contiene los siguientes elementos:

Trabaja con una fuente de datos real

Nuestro blog necesitará un lugar para escribir y leer entradas nuevas. Para demostrar cómo trabajar con un servicio de datos real, usaremos la API de Google Apps Spreadsheets. Esto nos permite leer fácilmente el contenido de cualquier hoja de cálculo creada con Documentos de Google.

Sigue estos pasos para configurarlo:

  1. En tu navegador (para estos pasos, se recomienda Chrome), abre esta hoja de cálculo de Documentos de Google. Contiene datos de publicaciones de muestra en los siguientes campos:

    • ID
    • Título
    • Autor
    • Contenido
    • Fecha
    • Palabras clave
    • Correo electrónico (del autor)
    • Slug (para la URL de slug de tu publicación)
  2. Ve al menú Archivo y selecciona Crear una copia para crear tu propia copia de la hoja de cálculo. Puedes editar el contenido cuando quieras, así como agregar o quitar publicaciones.

  3. Vuelve al menú Archivo y selecciona Publicar en la Web.

  4. Haz clic en Comenzar a publicar.

  5. En Obtener un vínculo a los datos publicados, en el último cuadro de texto, copia la parte de la clave de la URL proporcionada. Se ve de la siguiente manera: https://docs.google.com/spreadsheet/ccc?key=0AhcraNy3sgspdDhuQ2pvN21JVW9NeVA0M1h4eGo3RGc#gid=0

  6. Pega la clave en la siguiente URL donde dice your-key-goes-here: https://spreadsheets.google.com/feeds/list/your-key-goes-here/od6/public/values?alt=json-in-script&callback=. Un ejemplo con la clave anterior podría ser https://spreadsheets.google.com/feeds/list/0AhcraNy3sgspdDhuQ2pvN21JVW9NeVA0M1h4eGo3RGc/od6/public/values?alt=json-in-script.

  7. Puedes pegar la URL en tu navegador y navegar a ella para ver la versión JSON del contenido de tu blog. Toma nota de la URL y, luego, dedica un poco de tiempo a revisar el formato de estos datos, ya que deberás iterar sobre ellos para mostrarlos en pantalla más adelante.

El resultado de JSON en tu navegador puede parecer un poco abrumador, pero no te preocupes. Solo nos interesan los datos de tus publicaciones.

La API de Google Spreadsheets genera cada uno de los campos de la hoja de cálculo de tu blog con un prefijo especial post.gsx$. Por ejemplo: post.gsx$title.$t, post.gsx$author.$t, post.gsx$content.$t, etcétera. Cuando iteremos sobre cada “fila” de nuestro resultado JSON, haremos referencia a estos campos para obtener los valores relevantes de cada publicación.

Ahora puedes editar el elemento de publicación que acabas de crear para vincular partes del lenguaje de marcado con los datos de tu hoja de cálculo. Para ello, introducimos un atributo post, que leerá el título, el autor, el contenido y otros campos de la publicación que creamos antes. El atributo selected (que completaremos más adelante) se usa para mostrar una publicación solo si un usuario navega al slug correcto para ella.

<polymer-element name="post-element" attributes="post selected">

    <template>

    <style>
        @host { :scope {display: block;} }
    </style>

        <div class="col-lg-4">

            <template if="[[post.gsx$slug.$t === selected]]">

            <h2>
                <a href="#[[post.gsx$slug.$t]]">
                [[post.gsx$title.$t  ]]
                </a>
            </h2>

            <p>By [[post.gsx$author.$t]]</p>

            <p>[[post.gsx$content.$t]]</p>

            <p>Published on: [[post.gsx$date.$t]]</p>

            <small>Keywords: [[post.gsx$keywords.$t]]</small>

            </template>

        </div>

    </template>

    <script>

    Polymer('post-element', {

        created: function() { },

        enteredView: function() { },

        leftView: function() { },

        attributeChanged: function(attrName, oldVal, newVal) { }

    });

    </script>

</polymer-element>

A continuación, ejecuta yo polymer:element blog para crear un elemento de blog que contenga una colección de entradas y el diseño de tu blog.

$ yo polymer:element blog

[?] Would you like to include constructor=''? No

[?] Import to your index.html using HTML imports? Yes

[?] Import other elements into this one? (e.g 'another_element.html' or leave blank) post.html

    create app/elements/blog.html

Esta vez, importamos el blog a index.html con importaciones de HTML, ya que queremos que aparezca en la página. En particular, para la tercera instrucción, especificamos post.html como el elemento que nos gustaría incluir.

Como antes, se crea un nuevo archivo de elemento (blog.html) y se agrega a /elements, esta vez importando post.html y agregando <post-element> dentro de la etiqueta de plantilla:

<link rel="import" href="post.html">

<polymer-element name="blog-element"  attributes="">

    <template>

    <style>
        @host { :scope {display: block;} }
    </style>

    <span>I'm <b>blog-element</b>. This is my Shadow DOM.</span>

        <post-element></post-element>

    </template>

    <script>

    Polymer('blog-element', {

        //applyAuthorStyles: true,

        //resetStyleInheritance: true,

        created: function() { },

        enteredView: function() { },

        leftView: function() { },

        attributeChanged: function(attrName, oldVal, newVal) { }

    });

    </script>

</polymer-element>

Como solicitamos que el elemento del blog se importe con importaciones de HTML (una forma de incluir y reutilizar documentos HTML en otros documentos HTML) a nuestro índice, también podemos verificar que se haya agregado correctamente al documento <head>:

<!doctype html>
    <head>

        <meta charset="utf-8">

        <meta http-equiv="X-UA-Compatible" content="IE=edge">

        <title></title>

        <meta name="description" content="">

        <meta name="viewport" content="width=device-width">

        <link rel="stylesheet" href="styles/main.css">

        <!-- build:js scripts/vendor/modernizr.js -->

        <script src="bower_components/modernizr/modernizr.js"></script>

        <!-- endbuild -->

        <!-- Place your HTML imports here -->

        <link rel="import" href="elements/blog.html">

    </head>

    <body>

        <div class="container">

            <div class="hero-unit" style="width:90%">

                <blog-element></blog-element>

            </div>

        </div>

        <script>
        document.addEventListener('WebComponentsReady', function() {
            // Perform some behaviour
        });
        </script>

        <!-- build:js scripts/vendor.js -->

        <script src="bower_components/polymer/polymer.min.js"></script>

        <!-- endbuild -->

</body>

</html>

Fantástico.

Agrega dependencias con Bower

A continuación, editemos nuestro elemento para usar el elemento de utilidad Polymer JSONP para leer posts.json. Puedes obtener el adaptador clonando el repositorio con Git o instalando polymer-elements a través de Bower con el comando bower install polymer-elements.

Dependencias de Bower

Una vez que tengas la utilidad, deberás incluirla como una importación en tu elemento blog.html con lo siguiente:

<link rel="import" href="../bower_components/polymer-jsonp/polymer-jsonp.html">

A continuación, incluye la etiqueta para ella y proporciona el url a nuestra hoja de cálculo de entradas de blog de antes, agregando &callback= al final:

<polymer-jsonp auto url="https://spreadsheets.google.com/feeds/list/your-key-value/od6/public/values?alt=json-in-script&callback=" response="[[posts]]"></polymer-jsonp>

Con esto en su lugar, ahora podemos agregar plantillas para iterar sobre nuestra hoja de cálculo una vez que se haya leído. La primera genera un índice con un título vinculado para una publicación que apunta a su slug.

<!-- Table of contents -->

<ul>

    <template repeat="[[post in posts.feed.entry]]">

    <li><a href="#[[post.gsx$slug.$t]]">[[post.gsx$title.$t]]</a></li>

    </template>

</ul>

La segunda renderiza una instancia de post-element para cada entrada encontrada y le pasa el contenido de la publicación según corresponda. Observa que pasamos un atributo post que representa el contenido de la publicación para una sola fila de la hoja de cálculo y un atributo selected que completaremos con una ruta.

<!-- Post content -->

<template repeat="[[post in posts.feed.entry]]">

    <post-element post="[[post]]" selected="[[route]]"></post-element>

</template>

El atributo repeat que ves que se usa en nuestra plantilla crea y mantiene una instancia con vinculaciones [[ ]] para cada elemento de la colección de arrays de nuestras publicaciones, cuando se proporciona.

App de Polymer

Ahora, para que se complete la [[route]] actual, haremos trampa y usaremos una biblioteca llamada Flatiron director, que se vincula a [[route]] cada vez que cambia el hash de la URL.

Afortunadamente, hay un elemento de Polymer (parte del paquete more-elements) que podemos usar para esto. Una vez que se copia en el directorio /elements, podemos hacer referencia a él con <flatiron-director route="[[route]]" autoHash></flatiron-director>, especificando route como la propiedad a la que deseamos vincularlo y decirle que lea automáticamente el valor de cualquier cambio de hash (autoHash).

Si unimos todo, obtenemos lo siguiente:

    <link rel="import" href="post.html">

    <link rel="import" href="polymer-jsonp/polymer-jsonp.html">

    <link rel="import" href="flatiron-director/flatiron-director.html">

    <polymer-element name="blog-element"  attributes="">

      <template>

        <style>
          @host { :scope {display: block;} }
        </style>

        <div class="row">

          <h1><a href="/#">My Polymer Blog</a></h1>

          <flatiron-director route="[[route]]" autoHash></flatiron-director>

          <h2>Posts</h2>

          <!-- Table of contents -->

          <ul>

            <template repeat="[[post in posts.feed.entry]]">

              <li><a href="#[[post.gsx$slug.$t]]">[[post.gsx$title.$t]]</a></li>

            </template>

          </ul>

          <!-- Post content -->

          <template repeat="[[post in posts.feed.entry]]">

            <post-element post="[[post]]" selected="[[route]]"></post-element>

          </template>

        </div>

        <polymer-jsonp auto url="https://spreadsheets.google.com/feeds/list/0AhcraNy3sgspdHVQUGd2M2Q0MEZnRms3c3dDQWQ3V1E/od6/public/values?alt=json-in-script&callback=" response="[[posts]]"></polymer-jsonp>

      </template>

      <script>

        Polymer('blog-element', {

          created: function() {},

          enteredView: function() { },

          leftView: function() { },

          attributeChanged: function(attrName, oldVal, newVal) { }

        });

      </script>

    </polymer-element>
App de Polymer

¡Bravo! Ahora tenemos un blog simple que lee datos de JSON y usa dos elementos de Polymer con scaffolding de Yeoman.

Cómo trabajar con elementos de terceros

El ecosistema de elementos en torno a los componentes web ha estado creciendo últimamente con sitios de galerías de componentes como customelements.io que comienzan a aparecer. Si revisamos los elementos creados por la comunidad, encontramos uno para recuperar perfiles de Gravatar, y podemos tomarlo y agregarlo también a nuestro sitio de blog.

Página principal de elementos personalizados

Copia las fuentes del elemento gravatar en tu directorio /elements, inclúyelo a través de importaciones de HTML en post.html y, luego, agrega a tu plantilla, pasando el campo de correo electrónico de nuestra hoja de cálculo como la fuente del nombre de usuario. ¡Eso!

<link rel="import" href="gravatar-element/src/gravatar.html">

<polymer-element name="post-element" attributes="post selected">

    <template>

    <style>
        @host { :scope {display: block;} }
    </style>

        <div class="col-lg-4">

            <template if="[[post.gsx$slug.$t === selected]]">

            <h2><a href="#[[post.gsx$slug.$t]]">[[post.gsx$title.$t]]</a></h2>

            <p>By [[post.gsx$author.$t]]</p>

            <gravatar-element username="[[post.gsx$email.$t]]" size="100"></gravatar-element>

            <p>[[post.gsx$content.$t]]</p>

            <p>[[post.gsx$date.$t]]</p>

            <small>Keywords: [[post.gsx$keywords.$t]]</small>

            </template>

        </div>

    </template>

    <script>

    Polymer('post-element', {

        created: function() { },

        enteredView: function() { },

        leftView: function() { },

        attributeChanged: function(attrName, oldVal, newVal) { }

    });

    </script>

</polymer-element>

Veamos qué nos da esto:

App de Polymer con elementos personalizados

¡Hermosa!

En un tiempo relativamente corto, creamos una aplicación sencilla compuesta por varios componentes web sin tener que preocuparnos por escribir código estándar, descargar dependencias de forma manual ni configurar un servidor local o un flujo de trabajo de compilación.

Optimiza tu aplicación

El flujo de trabajo de Yeoman incluye otro proyecto de código abierto llamado Grunt, un ejecutor de tareas que puede ejecutar una serie de tareas específicas de compilación (definidas en un Gruntfile) para producir una versión optimizada de tu aplicación. Si ejecutas grunt por sí solo, se ejecutará una tarea de default que el generador configuró para realizar análisis de lint, pruebas y compilaciones:

grunt.registerTask('default', [

    'jshint',

    'test',

    'build'

]);

La tarea jshint anterior verificará tu archivo .jshintrc para conocer tus preferencias y, luego, lo ejecutará en todos los archivos JavaScript de tu proyecto. Para obtener un resumen completo de tus opciones con JSHint, consulta la documentación.

La tarea test se ve un poco así y puede crear y entregar tu app para el framework de pruebas que recomendamos de forma predeterminada, Mocha. También ejecutará las pruebas por ti:

grunt.registerTask('test', [

    'clean:server',

    'createDefaultTemplate',

    'jst',

    'compass',

    'connect:test',

    'mocha'

]);

Como nuestra app en este caso es bastante simple, dejaremos que escribas las pruebas como un ejercicio separado. Hay algunos otros aspectos que nuestro proceso de compilación deberá controlar, así que veamos qué hará la tarea grunt build definida en nuestro Gruntfile.js:

grunt.registerTask('build', [

    'clean:dist',    // Clears out your .tmp/ and dist/ folders

    'compass:dist',  // Compiles your Sassiness

    'useminPrepare', // Looks for <!-- special blocks --> in your HTML

    'imagemin',      // Optimizes your images!

    'htmlmin',       // Minifies your HTML files

    'concat',        // Task used to concatenate your JS and CSS

    'cssmin',        // Minifies your CSS files

    'uglify',        // Task used to minify your JS

    'copy',          // Copies files from .tmp/ and app/ into dist/

    'usemin'         // Updates the references in your HTML with the new files

]);

Ejecuta grunt build y se compilará una versión de tu app lista para producción, que podrás enviar. Hagamos la prueba.

Compilación de Grunt

¡Listo!

Si te bloqueas, puedes consultar una versión compilada previamente de polymer-blog en https://github.com/addyosmani/polymer-blog.

¿Qué más tenemos preparado?

Los componentes web aún se encuentran en un estado de evolución y, por lo tanto, también lo están las herramientas que los rodean.

Actualmente, estamos analizando cómo se podrían concatenar las importaciones de HTML para mejorar el rendimiento de carga a través de proyectos como Vulcanize (una herramienta del proyecto Polymer) y cómo podría funcionar el ecosistema de componentes con un administrador de paquetes como Bower.

Te informaremos cuando tengamos mejores respuestas a estas preguntas, pero se avecinan muchos momentos emocionantes.

Instala Polymer de forma independiente con Bower

Si prefieres un inicio más ligero para Polymer, puedes instalarlo de forma independiente directamente desde Bower ejecutando lo siguiente:

bower install polymer

que lo agregará a tu directorio bower_components. Luego, puedes hacer referencia a él en el índice de tu aplicación de forma manual y triunfar en el futuro.

¿Qué opinas?

Ahora sabes cómo estructurar una app de Polymer con componentes web con Yeoman. Si tienes comentarios sobre el generador, háznoslo saber en los comentarios, envía un informe de errores o publica en la herramienta de seguimiento de errores de Yeoman. Nos encantaría saber si hay algo más que te gustaría que el generador hiciera mejor, ya que solo a través de tu uso y comentarios podemos mejorar :)