Cómo compilar aplicaciones web con Yeoman y Polymer

Crea andamios para tus aplicaciones web con herramientas modernas

Addy Osmani
Addy Osmani

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 modelo de texto correcto, configurar un flujo de trabajo de desarrollo y pruebas, y reducir y comprimir todas tus fuentes.

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

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 andamiaje que ofrece un ecosistema de andamiajes específicos del framework, 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 la administración de 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 de plantilla para tu app (o elementos individuales, como modelos), compilar tu Sass, minimizar y concatenar tu CSS, JS, HTML y tus imágenes, y activar un servidor web simple en tu directorio actual. También puede ejecutar pruebas de unidades y mucho más.

Puedes instalar generadores desde Node Packaged Modules (npm) y, 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 sintaxis que permite el uso de componentes web en navegadores modernos. El proyecto permite a los desarrolladores compilar apps con la plataforma del mañana y comunicar al W3C los lugares en los que se pueden mejorar aún más las especificaciones de los servicios en vuelo.

Página principal del generador de Polymer

generator-polymer es un nuevo generador que te ayuda a crear andamios para apps de Polymer con Yeoman, lo que te permite crear y personalizar fácilmente elementos de Polymer (personalizados) 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 de plantilla por ti.

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

$ npm install generator-polymer -g

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

El generador que instalamos recientemente tiene algunos datos específicos a los que tendrás acceso:

  • polymer:element se usa para crear andamios de nuevos elementos individuales de Polymer. Por ejemplo: yo polymer:element carousel.
  • polymer:app se usa para crear un andamiaje de tu index.html inicial, un Gruntfile.js que contiene la configuración del tiempo de compilación de tu proyecto, así como 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.

Construyamos una app de Polymer

Vamos a crear un blog simple con algunos elementos de Polymer personalizados y nuestro nuevo generador.

App de Polymer

Para comenzar, ve a la terminal, crea un directorio nuevo y ábrelo con 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 crea un andamiaje de un index.html, una estructura de directorio y tareas de Grunt para tu flujo de trabajo. ¿Por qué no tomas un café mientras esperamos a que la app termine de prepararse?

Bien, ahora podemos ejecutar grunt server para obtener una vista previa de cómo se ve la app:

Servidor de Grunt

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

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

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

Yeoman nos hace algunas preguntas, como si queremos incluir un constructor o usar una importación de HTML para incluir el elemento de publicación en index.html. Por ahora, digamos que 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 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:

Cómo trabajar con una fuente de datos real

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

Comencemos con la configuración:

  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 del 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 y agregar o quitar publicaciones.

  3. Vuelve al menú File y selecciona Publish to the web.

  4. Haz clic en Iniciar publicación.

  5. En Obtener un vínculo a los datos publicados, en el último cuadro de texto, copia la parte 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 que usa 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. Anota la URL y, luego, tómate un momento para revisar el formato de estos datos, ya que deberás iterar sobre ellos para mostrarlos en pantalla más adelante.

Es posible que el resultado de JSON en tu navegador parezca un poco abrumador, pero no te preocupes. Solo nos interesan los datos de tus publicaciones.

La API de Hojas de cálculo de Google muestra 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 por cada “fila” en nuestro resultado JSON, haremos referencia a estos campos para recuperar los valores relevantes de cada publicación.

Ahora puedes editar el elemento de publicación con andamiaje nuevo para vincular partes del marcado a los datos de tu hoja de cálculo. Para ello, agregamos un atributo post, que leerá el título de la publicación, el autor, el contenido y otros campos que creamos antes. El atributo selected (que propagaremos más adelante) se usa para mostrar solo una publicación si un usuario navega al fragmento 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, ejecutemos yo polymer:element blog para crear un elemento de blog que contenga una colección de publicaciones 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 como nos gustaría que apareciera en la página. Específicamente, para la tercera instrucción, especificamos post.html como el elemento que queremos incluir.

Como antes, se crea un nuevo archivo de elementos (blog.html) y se agrega a /elements. Esta vez, se importa post.html y se incluye <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 pedimos que el elemento del blog se importe a nuestro índice con importaciones de HTML (una forma de incluir y reutilizar documentos HTML en otros documentos HTML), 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.

Cómo agregar dependencias con Bower

A continuación, editemos nuestro elemento para usar el elemento de utilidad JSONP de Polymer y leer posts.json. Para obtener el adaptador, clona el repositorio con git o instala polymer-elements a través de Bower ejecutando bower install polymer-elements.

Dependencias de Bower

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

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

A continuación, incluye la etiqueta y proporciona el url a nuestra hoja de cálculo de entradas de blog anterior, y agrega &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, ahora podemos agregar plantillas para iterar en nuestra hoja de cálculo una vez que se haya leído. El primero genera un índice, con un título vinculado para una publicación que apunta al fragmento correspondiente.

<!-- 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>

El segundo 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 de una sola fila de la hoja de cálculo y un atributo selected que propagaremos 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 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 propague la [[route]] actual, vamos a hacer trampa y usar una biblioteca llamada Flatiron Director, que se vincula a [[route]] cada vez que cambia el hash de la URL.

Por suerte, hay un elemento de Polymer (parte del paquete more-elements) que podemos usar. 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 queremos vincularlo y diciéndole que lea automáticamente el valor de cualquier cambio de hash (autoHash).

Si juntamos 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 andamiaje de Yeoman.

Cómo trabajar con elementos de terceros

El ecosistema de elementos alrededor de los componentes web ha crecido últimamente, y han comenzado a aparecer sitios de galerías de componentes como customelements.io. Mientras revisaba los elementos creados por la comunidad, encontré uno para recuperar perfiles de gravatar. También podemos tomarlo y agregarlo a nuestro sitio de blog.

Página principal de elementos personalizados

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

<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 brinda esto:

App de Polymer con elementos personalizados

¡Hermosa!

En un tiempo relativamente corto, creamos una aplicación simple compuesta por varios componentes web sin tener que preocuparnos por escribir código de plantilla, descargar dependencias de forma manual o 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 varias 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 default que el generador configuró para lint, pruebas y compilación:

grunt.registerTask('default', [

    'jshint',

    'test',

    'build'

]);

La tarea jshint anterior verificará tu archivo .jshintrc para conocer tus preferencias y, luego, la 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 de la siguiente manera y puede crear y entregar tu app para el framework de pruebas que recomendamos de forma predeterminada, Mocha. También ejecutará tus pruebas por ti:

grunt.registerTask('test', [

    'clean:server',

    'createDefaultTemplate',

    'jst',

    'compass',

    'connect:test',

    'mocha'

]);

Como nuestra app en este caso es bastante simple, te dejaremos la tarea de escribir pruebas como un ejercicio independiente. Hay 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 debería compilar una versión de tu app lista para enviar a producción. Vamos a probarlo.

Compilación de Grunt

¡Listo!

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

¿Qué más tenemos en la tienda?

Los componentes web aún están en 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 el ecosistema de componentes podría funcionar con un administrador de paquetes como Bower.

Te informaremos cuando tengamos mejores respuestas a estas preguntas, pero te aseguramos que te esperan muchos momentos emocionantes.

Cómo instalar Polymer independiente con Bower

Si prefieres un inicio más ligero de Polymer, puedes instalarlo de forma independiente directamente desde Bower. Para ello, ejecuta 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 arrasar en el futuro.

?

Ahora sabes cómo crear un andamiaje para una app de Polymer con componentes web con Yeoman. Si tienes comentarios sobre el generador, infórmanos en los comentarios, informa un error o publica en el seguimiento de problemas 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 tus comentarios podemos mejorar :)