Tutorial. Entorno con Taskr, Rollup y Yarn

11/02/2017

¿Cansado de utilizar grunt/gulp para tus proyectos web? ¿Has intentado dar el salto a webpack pero te ha parecido engorroso y poco intuitivo? No te preocupes. Hoy te presentamos Taskr, una alternativa que te permitirá realizar proyectos sencillos con una curva de aprendizaje muy baja.

Taskr es un generador de tareas/rutinas que se caracteriza por su sencillez y velocidad con respecto a otras alternativas. Estaba un poco abandonado, aunque en estos últimos meses ha podido resurgir de sus cenizas gracias a la ayuda de lukeed y otros tantos.

Antes que nada, necesitaremos una versión actualizada de node (mínimo 6.9), es importante recalcar esto para evitarnos sorpresas inesperadas cuando arranquemos con esto. En este tutorial os explicaremos como usarlo de manera rápida y clara para un proyecto de AngularJs como ejemplo. Sin más preámbulos, comenzamos.

Es un pájaro, es un avión, no... Es Taskr!

Lo primero que tendremos que hacer es crearnos una carpeta llamada Taskr, en ella generaremos un package.json y un bower.json con los comandos npm init y bower init respectivamente. En estos ficheros instalaremos todo aquello que sea necesario para montar nuestra web, es importante recalcar que tendreis que instalar npm y bower previamente.

A continuación, crearemos una carpeta src en la carpeta Taskr donde alojaremos las carpetas divididas según el tipo de ficheros que contengan: javascript, css, html, imagenes y vendors. Más adelante explicaremos que ficheros contendrán dichas carpetas con sus correspondientes rutas para no liarnos.

Ahora pasaremos a generar nuestro taskfile.js en src, este fichero actuara como archivo de configuración cuando ejecutemos el comando taskr. En este fichero se escribirán las tareas que necesitemos. La estructura de ficheros y carpetas quedaría del siguiente modo:

    Taskr/ 
    /* Carpeta raiz */ 
    --- src/ 
    /* Carpeta src */ 
    -------- /css 
    /* Los estilos */ 
    -------- /html 
    /* Los htmls */
    -------- /img 
    /* Las imagenes */ 
    -------- /js 
    /* Los javascripts */ 
    -------- /vendors 
    /* Librerias externas */ 
    --- package.json 
    --- bower.json 
    --- taskfile.js

Bueno, ahora pasamos al quid de la cuestión. Nuestra aplicación de ejemplo estará basada en AngularJs como ya he dicho anteriormente con las siguientes características: tendrá dos páginas y estará en español/ingles. Por si esto no fuera suficiente, incluiremos librerías externas como leaflet para que muestren un mapa y darle un poco de vidilla. Todo esto lo instalaremos con los siguientes comandos desde la carpeta Taskr:

/* Instalamos angular y otros componentes relacionados con npm */ npm install --save angular angular-ui-router
    angular-translate /* Instalamos leaflet y su directiva para angular con bower. Escoger la 1.0.0 */ bower install
    --save leaflet angular-leaflet-directive

Ahora toca lo más importante. Instalamos Taskr junto a los siguientes plugins para echar a rodar el proyecto -> @taskr/esnext: nos permitirá usar sintaxis ES6/7 en nuestro taskfile.js y crear tareas asíncronas. @taskr/clear: plugin para realizar borrados de ficheros. @taskr/watch: plugin que nos permite lanzar tareas cuando se produzcan modificaciones. Este sería el comando para empezar a “volar”:

/* Instalamos taskr y algunos de sus plugins */ npm install --save-dev taskr @taskr/esnext @taskr/clear @taskr/watch

taskfile.js: Retomamos con el archivo de configuración para centrarnos en su contenido. Este fichero se dividirá en 3 partes: (1) Declaración de variables, (2) Tarea default que se ejecutara al arrancar con el comando taskr por terminal y (3) Definición de nuestras propias tareas:

/* (1) Declaración de variables */ /* Tarea default */ export default async function (task) {
  ' '
}
{
  /* (2) Arranque de tareas */
}
/* (3) Definición de tareas */

A partir de aquí nos centraremos en añadir tareas de forma acumulativa en la parte (3) desglosándolas en función del tipo de ficheros que vayamos a tratar (html, css, imagenes, js y vendors).

Después de esto, incluiremos eventos @taskr/watch por cada una de estas tareas en la tarea default (parte (2), que no es os olvide). De este modo, nuestras tareas se ejecutarán cada vez que modifiquemos los ficheros que nosotros fijemos, ya sea mediante rutas o por extensiones de ficheros. En la parte (1) se incluirán aquellos elementos ajenos a taskr y sus plugins pero que son fundamentales para generar nuestro proyecto. Os dejo a continuación las tareas que tendremos que añadir para realizar nuestra web de manera correcta según lo descrito anteriormente:

Browser-sync

Nuestra 1º tarea será montar todo nuestro proyecto en una carpeta denominada dist para posteriormente desplegarla y poder trabajar con ella en caliente. Esto no se conseguiría si no fuera por la inestimable ayuda de browser-sync y connect-history-api-fallback, así que pasamos a instalarlos:

/* Instalamos browser-sync y connect-history-api-fallback */ npm install --save-dev browser-sync
    connect-history-api-fallback

Tarea serve. Ahora toca modificar nuestro taskfile.js. Definiremos unas variables para agregar browser-sync y connect-history-api-fallback. Declararemos una tarea serve para poder inicializar nuestro servidor de "pruebas" en la carpeta dist por el puerto 3000. Finalmente añadiremos la tarea serve en la tarea default de taskfile.js para que arranque con task.start :

/* (1) Declaración de variables */ var bs = require('browser-sync'); var historyApiFallback = require('connect-history-api-fallback');

export default async function (task) {
  /* (2) Arranque de tareas */
  await task.start('serve')
}

/_ (3) Definición de tareas _/
export async function serve(task) {
bs({
server: 'dist'
, middleware: [historyApiFallback()]
});
}
Imagenes

Tarea copyImg. Copiaremos nuestras imágenes de src/img a dist/img, ningún misterio al respecto. La tarea se llamará copyImg que se encargará de borrar con clear cualquier imagen de dist, recolectar todas las imágenes de src/img con source y finalmente duplicarlas a la carpeta dist/img con target.

Añadiremos la tarea copyImg en la tarea default de taskfile.js y usaremos el evento task.watch para que se vuelva a ejecutar cada vez que detecte modificaciones en src/img:

export default async function (task){' '}
    {
      /* (2) Arranque de tareas */
      await task.watch('src/img/**/*.*', 'copyImg')
    }
    /* (3) Definición de tarea copyImg */ export async function copyImg(task) {await task.clear(['dist/img/**/*.*']).source('src/img/**/*.*').target('dist/img')}
Css

Ahora toca un tema peliagudo como son los estilos. Necesitaremos dos plugins de Taskr: @taskr/sass (para usar precompiladores como sass) y taskr-autoprefixer (para añadir autoprefixer). También instalaremos bootstrap-sass para tener algo de chicha y poder trabajar:

/* Instalamos @taskr/sass, taskr-autoprefixer y bootstrap */ npm install --save-dev @taskr/sass taskr-autoprefixer
    bootstrap-sass

src/css/app.sass: A continuación crearemos un fichero app.sass en la ruta src/css con los módulos necesarios para que nuestra web se muestre correctamente. Importaremos bootstrap desde node_modules y copiaremos el contenido del fichero bower_components/leaflet/dist/leaflet.css (previamente instalado con bower) en src/css/modules con el nombre leaflet.scss. Finalmente nuestro fichero app.sass quedaría del siguiente modo:

@import '../../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss' @import 'modules/leaflet.scss'

Tarea copyStyles. Esta tarea compilara el fichero src/css/app.sass gracias a los plugins @taskr/sass y taskr-autoprefixer dejando el resultado en la carpeta dist.

Solo falta añadir un @taskr/watch en la tarea default de taskfile.js para que se vuelva a construir nuestro css cuando detecte modificaciones en algún fichero scss de src/css o en src/css/app.sass. Nuestro browser-sync (bs) se encargará de refrescar el app.css de dist cada vez que se ejecute copyStyles:

export default async function (task) {
  /* (2) Arranque de tareas */
  await task.watch(['src/css/**/*.scss', 'src/css/app.sass'], 'copyStyles')
}

/* (3) Definición de tarea copyStyles */
export async function copyStyles(task) {
  await task.source('src/css/app.sass').sass().autoprefixer().target('dist')
  /* reload */
  bs.reload('app.css')
}
Vendors

Pasamos a las librerías externas. Necesitaremos el plugin @taskr/browserify para empaquetar todas estas librerías en un solo fichero vendors.js siendo posteriormente trasladado a la carpeta dist:

/* Instalamos @taskr/browserify */ npm install --save-dev @taskr/browserify

src/vendors/vendors.js: Aquí indicaremos la ruta de los ficheros que necesitamos obligatoriamente para dibujar nuestro mapa con leaflet y que previamente instalamos con bower: js require('../../bower_components/leaflet/dist/leaflet.js'); require('../../bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js');

Tarea copyVendors. Esta tarea cogerá el fichero src/vendors/vendors.js, lo empaquetara y lo entregara en dist. Añadiremos un @taskr/watch que lance copyVendors cada vez que modifiquemos el fichero src/vendors/vendors.js. Nuestro browser-sync (bs) se encargará de refrescar vendors.js de la carpeta dist cada vez que se ejecute copyVendors:

export default async function (task) {
  /* (2) Arranque de tareas */
  await task.watch('src/vendors/vendors.js', 'copyVendors')
}

/* (3) Definición de tareas */
export async function copyVendors(task) {
  await task.source('src/vendors/vendors.js').browserify().target('dist')
  /* reload */
  bs.reload('vendors.js')
}
Javascript

Usaremos nuevamente el plugin @taskr/browserify para empaquetar toda nuestra app con AngularJs pero con el añadido de usar babel para pasar nuestro código de ES6 a ES5. Necesitaremos instalar los siguientes paquetes:

/* Instalamos babelify y babel-preset-es2015 */ npm install --save-dev babelify babel-preset-es2015

Pasaremos a enumerar los 4 ficheros que contendrá src/js para completar el armazón de nuestra aplicación:

src/js/app.js. Este fichero importara angular y sus módulos (app.modules.js) junto a los dos controladores (header y home). Tan solo nos quedara configurar las traducciones y las rutas:

import * as App from './app.modules.js' import * as Home from './controllers/home-controller.js' import * as
Header from './controllers/header-controller.js' let app = angular.module('app', ['app.map', 'app.header', 'ui.router', 'pascalprecht.translate',
'leaflet-directive']) router.$inject = ['$stateProvider', '$urlRouterProvider']

function router($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise('/map/ivan')
    var mapState = {
        url: '/map/:id'
        , views: {
            '@': {
                templateUrl: './views/map.html'
            }
        }
    }
    var aboutState = {
        url: '/about'
        , views: {
            '@': {
                templateUrl: './views/about.html'
            }
        }
    }
    $stateProvider.state('map', mapState)
    $stateProvider.state('about', aboutState)
}
translation.$inject = ['$translateProvider']

function translation($translateProvider) {
$translateProvider.translations('es', {
MAP: 'Mapa'
, ABOUT: 'Sobre mi'
, BUTTON_LANG_EN: 'ingles'
, BUTTON_LANG_ES: 'español'
})
$translateProvider.translations('en', {
MAP: 'Map'
, ABOUT: 'About me'
, BUTTON_LANG_EN: 'english'
, BUTTON_LANG_ES: 'spanish'
})
$translateProvider.preferredLanguage('es')
}
app.config(router).config(translation)

src/js/app.modules.js: Aquí importaremos angular, angular-ui-router y angular-translate. Este fichero nos servirá también para registrar los módulos que empleemos. app.map será el modulo del controlador para visualizar el mapa y app.header se encargara de permitirnos cambiar el idioma:

import angular from 'angular'
import router from 'angular-ui-router'
import translate from 'angular-translate'

/_ register Modules _/
angular.module('app.map', [])
angular.module('app.header', [])

src/js/controllers/header-controller.js. Este es el modulo app.header con un constructor headerController que tiene una función para cambiar el idioma:

angular.module('app.header').controller('headerController', [
  '$scope',
  '$translate',
  function ($scope, $translate) {
    $scope.changeLanguage = function (key) {
      $translate.use(key)
    }
  },
])

src/js/controllers/home-controller.js. Este es el modulo app.map con un constructor homeController que tiene un centro con sus coordenadas y un id para visualizar el parámetro id de la url map/:id:

angular.module('app.map').controller('homeController', [
  '$scope',
  '$stateParams',
  function ($scope, $stateParams) {
    $scope.center = {
      lat: 51.505,
      lng: -0.09,
      zoom: 8,
    }

    let self = this
    self.id = $stateParams.id
  },
])

Tarea babel. Esta tarea compilara el fichero src/js/app.js gracias al plugin @taskr/browserify dejando el resultado en la carpeta dist. Solo falta añadir un @taskr/watch en la tarea default de taskfile.js para que se vuelva a construir nuestro app.js cuando detecte modificaciones en algún fichero js de src/js. Nuestro browser-sync (bs) se encargará de refrescar el app.js .

Usaremos una variable is_js para controlar modificaciones masivas de ficheros javascript, de esta forma nos aseguramos que construye la app una sola vez ante múltiples guardados simultáneos:

/* (1) Declaración de variables */
var is_js = 0

export default async function (task) {
  /* (2) Arranque de tareas */
  await task.watch('src/js/**/*.js', 'babel')
}

/* (3) Definición de tareas */
export async function babel(task) {
  is_js += 1
  if (is_js == 1) {
    await task
      .source('src/js/app.js')
      .browserify({
        transform: [
          require('babelify').configure({
            presets: ['es2015'],
          }),
        ],
      })
      .target('dist')
  }
  is_js -= 1
  if (is_js == 0) bs.reload('app.js')
}
Html

Tarea copyHtml. Copiaremos nuestros htmls de src/html a dist. La tarea se llamará copyHtml que se encargará de borrar con clear cualquier html de dist, recolectar todas los htmls de src/html con source y finalmente duplicarlas a la carpeta dist con target:

export default async function (task) {
  /* (2) Arranque de tareas */
  await task.watch('src/html/**/*.html', 'copyHtml')
}

/_ (3) Definición de tareas _/
export async function copyHtml(task) {
await task.clear(['dist/**/*.html']).source('src/html/**/_.html').target('dist')
/_ reload \*/
bs.reload('**/\*.html')
}

src/html/index.html. Fichero principal que tirara del app.js y vendors.js creados por las tareas babel y copyVendors. Se usará un componente commons/menu.html para representar un menu de navegacion. Por último, incluiremos el app.css creado por copyStyles en el index:

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Angular</title>
    <link rel="stylesheet" href="app.css"> </head> <body ng-app="app"> <div class="container">
    <ng-include src="'./commons/menu.html'"></ng-include> <ui-view></ui-view> </div> <script
    src="app.js"></script> <script src="vendors.js"></script> </body> </html>

src/html/views/map.html. La vista del mapa con su correspondiente etiqueta leaflet para representar nuestro mapa:

<div ng-controller="homeController as hd">
    <h1>{% raw />{{ 'MAP' | translate }}{% endraw /> - id:{% raw />{{hd.id}}{% endraw /></h1>
        <leaflet lf-center="center" height="480px"></leaflet>
</div>

src/html/views/about.html. Una vista muy sencilla que no necesita mucha explicación al respecto:

<h1>{% raw />{{ 'ABOUT' | translate }}{% endraw /></h1> 

src/html/commons/menu.html. El menú de navegación para cambiar el idioma y alternar vistas según queramos:

<div ng-controller="headerController">
    <ol class="breadcrumb">
        <li><a ui-sref="map" ui-sref-active="active">{% raw />{{ 'MAP' | translate }}{% endraw /></a></li>
        <li><a ui-sref="about" ui-sref-active="active">Tutorial</a></li>
        <li><a ng-click="changeLanguage('en')">{% raw />{{ 'BUTTON_LANG_EN' | translate }}{% endraw /></a></li>
        <li><a ng-click="changeLanguage('es')">{% raw />{{ 'BUTTON_LANG_ES' | translate }}{% endraw /></a></li>
    </ol>
</div>  
Yarn

Habéis podido comprobar que nos hemos centrado en incluir eventos @taskr/watch dentro de la tarea default. Eso está muy bien para recargarnos nuestro proyecto cada vez que cambiemos alguna cosa, pero se nos olvida lo más fundamental, construirlo todo nada más arrancarlo.

Tarea build. Esta tarea ejecutara en paralelo todas las tareas definidas anteriormente para construirlo todo en la carpeta dist. Usaremos task.clear para eliminar cualquier contenido previo de dist y lanzaremos un task.parallel para reconstruirlo todo. No debemos olvidarnos de incluir un evento task.start en la tarea default para arrancarla desde el inicio:

export default async function (task) {
  /* (2) Arranque de tareas */
  await task.start('build')
}

/_ (3) Definición de tareas _/
export async function build(task) {
await task.clear(['dist']).parallel(['copyVendors', 'babel', 'copyHtml', 'copyImg', 'copyStyles'])
}

Ya casi hemos terminado, solo nos falta añadir algunos scripts en nuestro package.json. start: para lanzar la tarea default del archivo de configuración taskfile.js, server para lanzar la web en un navegador y download para descargar todos los paquetes necesarios. En este último script se ha reemplazado npm por yarn:

"scripts": {
    "start": "taskr",
    "server": "taskr serve",
    "download": "yarn install && bower install"
  }

Yarn es un instalador de paquetes creado por Facebook en octubre de 2016 que se diferencia de npm por su alto desempeño alcanzando mejores tiempos que su competidor. Podéis instalarlo con un simple npm install -g yarn, os dejo los tiempos que obtuve descargando todos los paquetes con ambos en mi equipo:

/* Tiempo con npmn sin cache */ 50.235s /* Tiempo con npm con cache */ 27.184s /* Tiempo con yarn sin cache */
    17.67s /* Tiempo con yarn con cache */ 9.40s
Rollup

Para ir terminando podemos incluir una tarea bundle con ayuda de Rollup para disminuir el tamaño nuestra app. Usaremos el plugin fly-rollup junto a algunos plugins de Rollup: uglify para disminuir su tamaño, commonjs para convertir modulos commonjs a ES6 y node-resolve para usar modulos de terceros descargados en node_module:

/* Instalamos algunos plugins de rollup */ npm install --save-dev fly-rollup rollup-plugin-babel
    rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-uglify

Tarea bundle. Ok, tan solo tendremos que aplicar todos los plugins de Rollup en src/js/app.js y dejar el resultado en una nueva carpeta de nombre bundle:

export async function
bundle(task) {await task
  .source('src/js/app.js')
  .rollup({
    rollup: {
      plugins: [
        require('rollup-plugin-babel')({
          presets: [['es2015', { modules: false }]],
        }),
        require('rollup-plugin-node-resolve')({ main: true, jsnext: true, browser: true }),
        require('rollup-plugin-commonjs')(),
        require('rollup-plugin-uglify')(),
      ],
    },
    bundle: { format: 'iife' },
  })
  .target('bundle')}

Es una gozada ver lo rapido que responde

Conclusiones: La verdad es que Taskr sorprende gratamente, con un fichero muy pequeño (70 lineas) tenemos montado un entorno para trabajar la mar de sencillo. Más rápido que gulp y más sencillo de comprender que webpack, no se puede pedir más por menos. Lo único que se le puede achacar es su escasez de plugins pero supongo que con el tiempo acabaran apareciendo nuevos. Dejo el código como siempre: