jueves, 25 de febrero de 2016

Prácticas Recomendadas para Proyectos con AngularJS 1.x Parte I: Estructura del Proyecto, Secciones y Rutas


Introducción

AngularJS es uno de los frameworks JavaScript que más popularidad ha alcanzado, no sólo por llevar conceptos como Inyección de Dependencias y Modelo Vista Controlador (MVC) al desarrollo con JavaScript (ya que otros frameworks como Ember.js o Backbone.js tienen algunas de esas características) sino que también tiene el soporte de una organización como Google, lo cual genera cierta confianza junto con el creciente número de la comunidad de desarrolladores que lo usan.

Sin embargo es relativamente común encontrar comentarios, principalmente de quienes venimos de trabajar con lenguajes y frameworks más estructurados (Java, C# o inclusive Ruby), que indican que desarrollar aplicaciones web de tamaño y complejidad considerable usando estos frameworks JavaScript terminan siendo difíciles de mantener en el mediano plazo debido no solamente a la naturaleza dinámica de JavaScript como lenguaje, sino también a la falta de consenso en la adopción de prácticas entre las comunidades de desarrolladores.

Google en su momento publicó una serie de estándares con estilos y mejores prácticas tanto en código JavaScript como en la estructura de carpetas y archivos para desarrollo de aplicaciones web usando AngularJS, que aunque no son de obligatorio seguimiento tenían como propósito unificar un poco los criterios de la comunidad desarrolladora y proponer un esquema más fácil de mantener en todo el ciclo de vida las aplicaciones.

Actualmente el propio equipo de Google recomienda usar los estándares propuestos por la comunidad (principalmente John Papa y Todd Motto), lo cuales aunque un poco largos definitivamente vale la pena revisarlos, aunque aquí se tratarán algunos allí documentados.

Teniendo en cuenta dichos estándares se desarrollará una pequeña aplicación para consultar el clima de una ciudad dentro de una lista disponible, con el propósito de mostrar cómo se pueden aplicar algunas de estas prácticas sugeridas.

Notas:
  • Al momento de redactar este post Angular 2 se encuentra en fase Beta, por lo cual se usará la versión 1.5

Estructura de Carpetas y Archivos

Generalmente las guías (tutoriales) de introducción a AngularJS tienen todo el código JavaScript dentro de las páginas HTML o se tienen en un sólo archivo por tipo de componente dentro de una carpeta "js" (controllers.js, directives.js, etc.) con el fin de simplificar los ejemplos desarrollados y permanecer más concentrados en los conceptos que se quieren enseñar, por ejemplo:
app/
----- index.html
----- css/
---------- styles.css
----- js/
---------- app.js
---------- controllers.js
---------- directives.js
---------- services.js
----- views/
---------- view1.html
---------- view2.html

Este esquema funciona bien para aplicaciones web pequeñas o para propósitos de aprendizaje, pero al momento de desarrollar aplicaciones grandes se termina con archivos de código grandes, difíciles de leer, entender y mantener.

En cuanto a la estructura de archivos, básicamente lo que se propone es tener una carpeta "app" con los archivos desarrollados para la aplicación (HTML, CSS y JavaScript), divididos a su vez en sub-carpetas por funcionalidad o sección de la aplicación, cada uno con una sola responsabilidad, por ejemplo:
app/
----- app.css
----- app.module.js
----- app.config.js
----- app.route.js
----- blocks/
---------- block1/
--------------- block1.module.js
--------------- block1.service.js
--------------- block1.service.spec.js
---------- block2/
--------------- block2.module.js
--------------- block2.provider.js
--------------- block2.provider.spec.js
----- core/
---------- core.module.js
---------- constants.js
---------- bussiness-logic1.service.js
---------- bussiness-logic1.service.spec.js
---------- bussiness-logic2.service.js
---------- bussiness-logic2.service.spec.js
----- index.html
----- layout/
---------- design-element.html
---------- design-element.controller.js
---------- design-element.controller.spec.js
----- section1/
---------- section1.controller.js
---------- section1.controller.spec.js
---------- section1.css
---------- section1.module.js
---------- section1.route.js
---------- section1.route.spec.js
--------------- subsection1-1/
-------------------- subsection1-1.controller.js
-------------------- subsection1-1.controller.spec.js
-------------------- subsection1-1.css
----- section2/
---------- section2.controller.js
---------- section2.controller.spec.js
---------- section2.css
---------- section2.module.js
---------- section2.route.js
---------- section2.route.spec.js
...
----- sectionN/
...
----- widgets/
---------- widget-name/
--------------- widget-name.html
--------------- widget-name.directive.js
--------------- widget-name.directive.spec.js
---------- widget-name2/
--------------- widget-name2.service.js
--------------- widget-name2.service.spec.js

Para cada archivo se tiene el siguiente propósito:
  • app/app.css: Estilos CSS comunes a toda la aplicación.
  • app/app.module.js: Definición del módulo principal de la aplicación.
  • app/app.config.js: Configuración del módulo principal de la aplicación.
  • app/app.route.js: Rutas generales de la aplicación.
  • app/blocks/: Contiene el conjunto de bloques o componentes de uso común en varias partes de la aplicación, como por ejemplo manejo de logs, seguridad, manejo de errores o eventos de analytics.
  • app/core/; Contiene los servicios y constantes que hagan parte de la lógica de negocio de la aplicación.
  • app/core/core.module.js: Define el módulo "core" de la aplicación. En algunos casos, en especial si se tienen muchas dependencias, este módulo puede referenciar las dependencias que se usen en los demás módulos de la aplicación, así cada módulo en lugar de referenciar cada una de estas, simplemente usa este módulo.
  • app/core/constants.js: Allí se tendrán las constantes que sean de uso compartido entre varios módulos de la aplicación.
  • app/core/bussiness-logic.service.spec.js: Cada elemento de AngularJS que se construya para la aplicación (directiva, controlador o servicio) puede tener pruebas unitarias, las cuales deben estar en un archivo con el mismo nombre del elemento desarrollado, pero con el sufijo ".spec". Estas pruebas unitarias van en la misma carpeta que el elemento probado no sólo para que sea fácil de ubicar sino también para que sea más inmediato ver que al cambiar el archivo con el código de la aplicación sea más fácil recordar que se deben tener actualizados con los cambios en el código. Las pruebas de integración (end-to-end) deben ir en una carpeta por fuera de "app/", por ejemplo "tests/" o "e2e/".
  • app/index.html: AngularJS es un framework basado en el principio de Aplicaciones de una sola Página (Single-page application), por lo que este será el punto de entrada a la aplicación
  • app/layout/: Los elementos visuales que definen un diseño común a toda la aplicación estarán en esta carpeta.
  • app/section1|2: Cada funcionalidad o sección dentro de la aplicación tendrá su carpeta en la raíz de "app/" y tendrá su propio módulo y rutas (si aplica), de esta manera es posible tratar de una manera isolada cada sección como una aplicación web más pequeña.
  • app/section1|2/section1|2.controller.js: En caso de los controladores, el archivo tendrá el sufijo ".controller" y sólo tendrá definida un controlador por archivo.
  • app/section1|2/section1|2.css: Estilos CSS usados solamente en la sección.
  • app/section1|2/section1|2.module.js: Módulo de la sección.
  • app/section1|2/section1|2.route.js: Rutas de la sección.
  • app/section1/subsection1-1: Cada sección puede tener sus propias sub-secciones o si se tienen pocas, todos los archivos pueden estar dentro de la sección contenedora. Como preferencia personal sugiero que así sean pocos archivos cada sub-sección tenga su propia carpeta.
  • app/widgets/: Carpeta con componentes gráficos comunes a varias secciones o controladores de la aplicación, por ejemplo directivas de elementos de UI. En la documentación con las recomendaciones de la estructura de proyectos esta carpeta aparece con el nombre "components", pero en este caso se usa "widgets" para evitar futuras confusiones dado que Angular 2 está usando ese nombre con otro propósito.

Puede parecer que son muchos archivos, pero esta estructura hace que cada archivo sea más corto, tenga solamente el código necesario para el módulo o sección al que pertenece y sea más fácil de mantener ya que las funcionalidades son más fáciles de buscar en el código, mientras que las imágenes y otros archivos de datos o configuración se almacenarían (idealmente) en una ubicación externa (Buckets de Amazon S3, a través de Amazon CloudFront por ejemplo). Básicamente favorece principios del desarrollo de software como la Separación de Responsabilidades, el Bajo Acoplamiento y la Alta Cohesión.


Desarrollo del Proyecto

Se desarrollará una aplicación web que en la página inicial presente una lista de ciudades con un enlace que lleve a otra página con el estado del clima actual en la ciudad seleccionada y adicionalmente una sección "About" (Acerca de).

Para ahorrar tiempo se usará como base el proyecto usando en el último post acerca de Gulp, el cual se puede descargar desde: https://github.com/guillermo-varela/gulp-bower-demo-ii

Borrar Contenido Previo

Sólo se deben borrar las carpetas "app/js" y "app/css".

Dependencias Bower

Para empezar se debe instalar AngularJS y AngularUI Router (ui-router) como dependencias web mediante Bower.
bower install --save angular#1.5.0 angular-ui-router

Aunque AngularJS tiene un módulo oficial para la configuración de las rutas en la aplicación (ngRoute), AngularUI Router se ha convertido en la opción preferida por la comunidad que usa AngularJS debido a que no sólo ofrece las mismas funcionalidades que el módulo oficial, sino que adiciona otras como rutas/vistas anidadas, rutas/vistas con nombre y su característica principal: manejo de máquinas de estado para las transiciones de rutas o vistas en la aplicación, lo cual permite tener cierta independencia de las URL usadas.

app/index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Weather Web App with AngularJS, Gulp and Bower">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Weather Web App</title>

  <!-- build:css css/vendor.min.css -->
  <!-- bower:css -->
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/vendor.min.js -->
  <!-- bower:js -->
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body ng-app="weatherApp">
  <!-- Navigation Bar -->
  <div ng-include="'layout/menu.html'"></div>

  <!-- Main Content -->
  <div class="container">
    <div ui-view></div>
  </div>

  <!-- Footer -->
  <div ng-include="'layout/footer.html'"></div>
</body>

</html>

Ahora que se tiene AngularJS pueden usarse sus directivas para empezar a darle forma al proyecto, empezando por el archivo "app/index.html" con los siguientes cambios con respecto al original:

  • Línea 8: Simplemente se actualiza la descripción de la aplicación.
  • Línea 11: Simplemente se actualiza el título de la página.
  • Líneas 13-31: Se remueven las referencias previas inyectadas por "gulp-inject" y "wiredep".
  • Línea 34: Se adiciona la directiva "ngApp" para indicar que el elemento "body" será la raíz de la aplicación AngularJS y el módulo será "weatherApp". Para aquellas personas nuevas en AngularJS, las directivas tienen su nombre en la definición del código y en la documentación usando Camel Case, pero al usarlas en HTML se usa guión como separador.
  • Línea 36: El fragmento HTML que definía el menú de navegación fue movido al archivo "layout/menu.html" y se incluye con la directiva "ngInclude". Nótese que la ruta indicada en la directiva está entre comillas simples, esto se debe a que "ngInclude" espera que el valor sea una expresión, por lo que usándolas se indica que el valor es simplemente una cadena de texto.
  • Línea 40: El contenido estático que se tenía fue reemplazado por "<div ui-view></div>". La directiva "ui-view" se usa para indicar dónde se deben poner las vistas usando AngularUI Router.
  • Línea 44: Se adiciona un pie de página (footer) referenciando el archivo "layout/footer.html".

app/layout/menu.html

<nav class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">

    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">Weather Web App</a>
    </div>

    <div id="navbar" class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li ui-sref-active="active"><a ui-sref="home">Home</a></li>
        <li ui-sref-active="active"><a ui-sref="about">About</a></li>
      </ul>
    </div>

  </div>
</nav>

Básicamente es el mismo menú de navegación que se tenía originalmente en "index.html", pero ahora usando directivas de AngularUI Router para los enlaces del menú:
  • ui-sref: Genera un enlace asociado al estado indicado en el valor, en este caso "home" y "about".
  • ui-sref-active: Cuando la aplicación se encuentre en el estado indicado en la directiva "ui-sref" del propio elemento HTML o su elemento padre/contenedor (como es el caso actual) se adicionará la clase CSS al elmento en que se usa. De esta manera cuando el usuario se encuentre en "Home" será dicho enlace el que esté resaltado e igualmente cuando se encuentre en "About".


app/layout/footer.html

<footer class="footer">
  <div class="container">
    <p class="navbar-text">Copyright &copy; 2016</p>
  </div>
</footer>

La razón para tener tanto el menú de navegación como el pie de página como HTML externos (partials) e incluirlos usando la directiva "ngInclude" en "index.html" en lugar de definirlos como directivas es que estos no se estarán usando en otras partes de la aplicación, sólo serán usados una vez en "index.html". Este es un punto que al momento de escribir este post aún se encuentra en discusión, pero por ahora personalmente prefiero que los elementos a incluir una vez usen ngInclude y los demás directivas.

app/app.css

html {
  position: relative;
  min-height: 100%;
}

body {
  padding-top: 50px;
  /* Margin bottom by footer height */
  margin-bottom: 60px;
}

.title {
  text-align: center;
}

.footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  /* Set the fixed height of the footer here */
  height: 60px;
  background-color: #f5f5f5;
}

.footer div p.navbar-text {
  float: none;
  text-align: center;
}

Este será el archivo que contenga los estilos CSS generales para la aplicación.

app/app.module.js

(function() {
  'use strict';

  /**
   * @ngdoc object
   * @name weatherApp
   *
   * @description
   * Main application module.
   */
  angular.module('weatherApp', []);
})();

Aquí se define el módulo principal de Angular para la aplicación. En todos los archivos JavaScript se usará Immediately Invoked Function Expression (IIFE), como lo indica la recomendación, para evitar exponer variables y funciones de manera global, además de facilitar la concatenación y minificación de los archivos JavaScript.

Los comentarios que se están usando para documentar el código son tags de "ngdoc", el cual es una versión ajustada a AngularJS de JSDoc.

.jscsrc

{
  "preset": "google",
  "maximumLineLength": {
    "value": 160,
    "allowComments": true,
    "allowRegex": true
  },
  "jsDoc": {
    "checkAnnotations": {
      "preset": "jsdoc3",
      "extra": {
        "ngdoc": true,
        "methodOf": true
      }
    }
  }
}

A la configuración de JSCS para la documentación usando JSDoc se le adicionan los tags propios de "ngdoc".

app/app.config.js

(function() {
  'use strict';

  angular
    .module('weatherApp')
    .config(configure);

  function configure($compileProvider) {
    // Replaced by Gulp build task
    $compileProvider.debugInfoEnabled('@@debugInfoEnabled' !== 'false');
    $logProvider.debugEnabled('@@debugLogEnabled' !== 'false');
  }
})();

En este archivo se realizarán las configuraciones del módulo principal de la aplicación que la afecten de manera global, como por ejemplo idiomas disponibles, inicialización de componentes o como en este caso indicar si la información de debug estará disponible o no.

La recomendación oficial es deshabilitar esta información para los ambientes de producción, así que más adelante se usará Gulp para reemplazar la cadena "@debugInfoEnabled" por "false" cuando se construya el proyecto para producción, así la versión de desarrollo/pruebas tendrá un resultado "true" para la comparación usada.

De manera similar durante el desarrollo y las pruebas todos los mensajes de logs que se envíen en modo "debug" estarán habilitados y podrán verse en el navegador web, pero en producción estos se deshabilitarán.

Otro punto a notar es que se está definiendo la configuración como una nueva función nombrada "configure" en lugar de tenerla como anónima como parámetro de "config" para que el código sea más fácil de leer, lo cual será una práctica recurrente en todo el código JavaScript.


Orden de los Archivos Inyectados

Al intentar ejecutar "gulp" para inyectar los archivos JavaScript, CSS, dependecias de Bower e iniciar el servidor local de desarrollo se puede ver el siguiente error: "Module 'weatherApp' is not available!"

Figura 1 - Error al Cargar la Aplicación

Esto se debe a que en la tarea "inject" de Gulp se está generando lo siguiente en "index.html":
<!-- build:js js/scripts.min.js -->
<!-- inject:js -->
<script src="app.config.js"></script>
<script src="app.module.js"></script>
<!-- endinject -->
<!-- endbuild -->

El archivo "app.module.js" debe incluirse primero ya que es el que define el módulo de la aplicación. Para asegurar que los archivos se inyectan en el orden correcto se usará el plugin "gulp-angular-filesort", el cual se puede instalar con el siguiente comando:
npm install --save-dev gulp-angular-filesort

Dado que tiene el prefijo "gulp-" este será cargado automáticamente por "gulp-load-plugins", así que se puede usar en la tarea "inject" en "gulpfile.js" de la siguiente manera:
...
// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('./index.html', {cwd: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app}).pipe(plugins.angularFilesort()), {
        relative: true
    }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
    }))
    .pipe(gulp.dest(paths.app));
});
...

Al ejecutar nuevamente "gulp" el archivo "app.module.js" estará referenciado primero y la aplicación ya no debe presentar error alguno.

Sección Home

Será la sección en la cual el usuario iniciará al abrir la aplicación. Para esta sección se creará una carpeta "app/home" con los siguientes archivos:

app/home/home.module.js
(function() {
  'use strict';

  /**
   * @ngdoc overview
   * @name weatherApp.home
   * @requires ui.router
   *
   * @description
   * Module for the home section.
   */
  angular.module('weatherApp.home', ['ui.router']);

})();

Como se indica en la recomendación, cada sección representativa de la aplicación debe estar en su propio módulo de AngularJS el cual debe ser tan independiente de las otras secciones que pueda usarse y probarse de una manera independiente de las demás secciones.

app/home/home.route.js
(function() {
  'use strict';

  angular
    .module('weatherApp.home')
    .config(configuration);

  /* @ngInject */
  function configuration($stateProvider, $urlRouterProvider) {
    $stateProvider
      .state('home', {
        url: '/home',
        templateUrl: 'home/home.html'
      });

    $urlRouterProvider.otherwise('/home');
  }
})();

Al tener un módulo por aparte para esta sección, también se tiene una definición propia de las rutas/estados del módulo. En este caso sólo se tiene una ruta/estado, pero secciones con muchas opciones o sub-secciones sí llegarían a tener un número considerable.

Nótese que se está adicionando el comentario "/* @ngInject */" al inicio de la función "configuration". Esto se debe a que más adelante se usará el plugin "gulp-ng-annotate" para generar las anotaciones de las dependencias al momento de hacer la minificación del código y aunque dicho plugin puede detectar automáticamente varias de las funciones que lo requieren no siempre es el caso por lo que se recomienda usar el comentario para cubrir todos los casos posibles.


app/home/home.html
<div class="jumbotron text-center">
  <h1>Available Cities</h1>
  <p>Select the city to check the current weather.</p>
</div>

app/about/about.module.js
(function() {
  'use strict';

  /**
   * @ngdoc overview
   * @name weatherApp.about
   * @requires ui.router
   *
   * @description
   * Module for the about section.
   */
  angular.module('weatherApp.about', ['ui.router']);

})();

app/about/about.route.js
(function() {
  'use strict';

  angular
    .module('weatherApp.about')
    .config(configuration);

  /* @ngInject */
  function configuration($stateProvider) {
    $stateProvider
      .state('about', {
        url: '/about',
        templateUrl: 'about/about.html'
      });
  }
})();

app/about/about.html
<div class="jumbotron text-center about-header">
  <h1>About this Site</h1>
  <p>This site is just a demonstration for AngularJS 1.5 with an organized project structure using Gulp.</p>
</div>

app/about/about.css
.about-header p {
  color: deepskyblue;
}

Se usa un archivo CSS para esta sección simplemente para ilustrar que cada sección puede tener sus propios estilos.

app/app.module.js
(function () {
  'use strict';

  /**
   * @ngdoc object
   * @name weatherApp
   * @requires weatherApp.home
   * @requires weatherApp.about
   *
   * @description
   * Main application module.
   */
  angular.module('weatherApp', [
    /* Feature areas */
    'weatherApp.home',
    'weatherApp.about'
  ]);
})();

Cada módulo dentro de la aplicación debe incluirse como una dependencia del módulo principal.

Al probar nuevamente la aplicación en el navegador web ya podrán verse ambas secciones:

Figura 2 - Sección Home

Figura 3 - Sección About

Título de la Página

Como puede verse en las figuras 2 y 3, el título de las páginas en el navegador siempre es "Weather Web App", debido a que así se tiene en "index.html".

Dependiendo del proyecto esto puede ser un dato menor e irrelevante, aunque en otros puede llegar a quererse que se tengan títulos diferentes por página/sección, no sólo para que el historial de navegación del usuario sea más claro, sino también para que sea más fácil identificar cada sección al momento de adicionarlos a la lista/barra de favoritos (bookmarks) del navegador.

Para lograr esto se requieren algunos ajustes:

app/index.html
<!DOCTYPE html>
<html ng-app="weatherApp">
...
<body>
...
  <title ng-bind="pageTitle"></title>
...
<body>
...

La directiva "ngApp" se debe mover de la etiqueta "body" a "html" para indicar que toda podrá ser procesada por AngularJS. Adicionalmente se indica que el valor a usar como título estará asociado a la variable "pageTitle" mediante la directiva "ngBind". En este caso no se usa una expresión para que el usuario no vea cambiar tan bruscamente (ni con intermitencias) el título de las páginas.

app/home/home.route.js
(function() {
  'use strict';

  angular
    .module('weatherApp.home')
    .config(configuration)
    .run(runModule);

  /* @ngInject */
  function configuration($stateProvider, $urlRouterProvider) {
    $stateProvider
      .state('home', {
        url: '/home',
        templateUrl: 'home/home.html',
        data: {
          pageTitle: 'Home'
        }
      });

    $urlRouterProvider.otherwise('/home');
  }

  /* @ngInject */
  function runModule($rootScope) {
    $rootScope.$on('$stateChangeSuccess',
      function (event, current, previous) {
        var title = 'Weather Web App';
        if (current.data && current.data.pageTitle) {
          title = current.data.pageTitle + ' - ' + title;
        }
        $rootScope.pageTitle = title;
      }
    );
  }
})();
  • Línea 7: Se adiciona una función para "run".
  • Líneas 25-33: Cada vez que se logre cambiar el estado de la aplicación (se acceda a una nueva ruta) de manera exitosa se genera el evento "$stateChangeSuccess" y el parámetro "current" es el estado al que ha entrado la aplicación. Cuando dicho evento se genere se actualizará el valor de la variable "pageTitle" (la que se espera en "index.html") usando el valor "data.pageTitle" del nuevo estado (si lo tiene) y el cambio se verá reflejado en el navegador.

app/home/home.route.js
(function() {
  'use strict';

  angular
    .module('weatherApp.about')
    .config(configuration)
    .run(runModule);

  /* @ngInject */
  function configuration($stateProvider) {
    $stateProvider
      .state('about', {
        url: '/about',
        templateUrl: 'about/about.html',
        data: {
          pageTitle: 'About'
        }
      });
  }

  /* @ngInject */
  function runModule($rootScope) {
    $rootScope.$on('$stateChangeStart',
      function (event, current) {
        var title = 'Weather Web App';
        if (current.data && current.data.pageTitle) {
          title = current.data.pageTitle + ' - ' + title;
        }
        $rootScope.pageTitle = title;
      }
    );
  }
})();

La razón de volver a definir la acción a tomar ante el evento "$stateChangeSuccess" en el módulo de la sección "About" (en lugar de por ejemplo tenerlo de manera global en "app.config.js") es que cada módulo debe ser auto-contenido e inclusive poderse aplicar en otro proyecto y seguir funcionando de manera consistente.

Al usar de nuevo el navegador podrá verse que ahora el título de la página cambia en cada sección:

Figura 4 - Sección Home con Título Propio

Figura 5 - Sección About con Título Propio

Usando una de las recomendaciones indicadas en la documentación, se usará un componente adicional para realizar la configuración de las rutas de la aplicación de manera consistente en todos lo módulos (provider):

app/blocks/router/router.module.js
(function() {
  'use strict';

  /**
   * @ngdoc overview
   * @name weatherApp.blocks.router
   * @requires ui.router
   *
   * @description
   * Module for the routing common configurations.
   */
  angular.module('weatherApp.blocks.router', ['ui.router']);
})();

app/blocks/router/router-helper-provider.jss
(function() {
  'use strict';

  angular
    .module('weatherApp.blocks.router')
    .provider('routerHelper', routerHelper);

  /* @ngInject */
  function routerHelper($stateProvider, $urlRouterProvider) {
    /* jshint validthis:true */
    this.$get = RouterHelper;

    /**
     * @ngdoc service
     * @name weatherApp.blocks.router.service:RouterHelper
     * @requires $state
     * @requires $rootScope
     *
     * @description
     * Helper with the routing common operations.
     */
    /* @ngInject */
    function RouterHelper($state, $rootScope) {
      var hasOtherwise = false;

      var service = {
        configureStates: configureStates,
        getStates: getStates
      };

      init();

      return service;

      ///////////////

      function init() {
        updateDocTitle();
      }

      /**
       * @ngdoc function
       * @name RouterHelper#configureStates
       * @methodOf weatherApp.blocks.router.service:RouterHelper
       *
       * @description
       * Finds a country with the given ID.
       *
       * @param {Array} states - Array with the states objects to configure.
       * @param {string=} otherwisePath - Optionally a default route path can be given.
       */
      function configureStates(states, otherwisePath) {
        states.forEach(function(state) {
          $stateProvider.state(state.state, state.config);
        });

        if (otherwisePath && !hasOtherwise) {
          hasOtherwise = true;
          $urlRouterProvider.otherwise(otherwisePath);
        }
      }

      /**
       * @ngdoc function
       * @name RouterHelper#getStates
       * @methodOf weatherApp.blocks.router.service:RouterHelper
       *
       * @description
       * Gets the configured states.
       *
       * @returns {Array} Configured states.
       */
      function getStates() {
        return $state.get();
      }

      /**
       * @ngdoc function
       * @name RouterHelper#updateDocTitle
       * @methodOf weatherApp.blocks.router.service:RouterHelper
       * @private
       *
       * @description
       * Updates the current page (document) title according to the state/route.
       */
      function updateDocTitle() {
        $rootScope.$on('$stateChangeStart',
          function(event, current) {
            var title = 'Weather Web App';
            if (current.data && current.data.pageTitle) {
              title = current.data.pageTitle + ' - ' + title;
            }
            $rootScope.pageTitle = title;
          }
        );
      }
    }
  }
})();

Cuando se define el servicio "RouterHelper" entre las líneas 24 y 33 no se está definiendo inmediatamente el cuerpo de las funciones que este tendrá y expondrá, sino que que simplemente se indican sus nombres y el nombre de la función que implementará la funcionalidad más abajo. Esto se hace para seguir otra de las recomendaciones para hacer más visible la interfaz de los componentes (operaciones disponibles) indicando sus nombres arriba y sus definiciones al final.

app/core/core.module.js
(function() {
  'use strict';

  /**
   * @ngdoc overview
   * @name weatherApp.core
   * @requires weatherApp.blocks.router
   *
   * @description
   * Module for the core bussiness logic and intra-app features.
   */
  angular.module('weatherApp.core', [
    /* Cross-app modules */
    'weatherApp.blocks.router'
  ]);
})();

Otra recomendación que se tiene es usar un módulo "core" para agrupar los módulos que sean dependencias de varios otros dentro de la aplicación, como es el caso del módulo "weatherApp.blocks.router", así como de tener la lógica de negocio de la aplicación.


app/home/home.module.js
(function() {
  'use strict';

  /**
   * @ngdoc overview
   * @name weatherApp.home
   * @requires weatherApp.core
   *
   * @description
   * Module for the home section.
   */
  angular.module('weatherApp.home', ['weatherApp.core']);

})();

Al tener el módulo "weatherApp.core" no es necesario que los módulos de cada sección incluyan la dependencias de "weatherApp.blocks.router" ni "ui.router".


app/home/home.route.js
(function () {
  'use strict';

  angular
    .module('weatherApp.home')
    .run(appRun);

  /* @ngInject */
  function appRun(routerHelper) {
    routerHelper.configureStates(getStates(), '/home');
  }

  function getStates() {
    return [
      {
        state: 'home',
        config: {
          url: '/home',
          templateUrl: 'home/home.html',
          data: {
            pageTitle: 'Home'
          }
        }
      }
    ];
  }
})();

  • Línea 6: Ahora sólo se requiere usar el método "run".
  • Línea 9: Se inyecta la dependencia al nuevo provider "routerHelper".
  • Línea 10: Mediante la función "configureStates" se indican los estados a configurar y adicionalmente la ruta que se debe tomar por defecto "/home", para conservar el comportamiento que se tenía previamente.
  • Líneas 13-26: Se declaran los estados/rutas que tendrá el módulo, en un arreglo de objetos con una estructura muy similar a la que se tenía previamente.


app/about/about.module.js
(function() {
  'use strict';

  /**
   * @ngdoc overview
   * @name weatherApp.about
   * @requires weatherApp.core
   *
   * @description
   * Module for the about section.
   */
  angular.module('weatherApp.about', ['weatherApp.core']);

})();

app/about/about.route.js
(function() {
  'use strict';

  angular
    .module('weatherApp.about')
    .run(appRun);

  /* @ngInject */
  function appRun(routerHelper) {
    routerHelper.configureStates(getStates());
  }

  function getStates() {
    return [
      {
        state: 'about',
        config: {
          url: '/about',
          templateUrl: 'about/about.html',
          data: {
            pageTitle: 'About'
          }
        }
      }
    ];
  }
})();

Nota: Una ventaja de estar usando Gulp es que a pesar de que en este punto ya se tiene una cantidad considerable de archivos, las tareas configuradas previamente en "gulpfile.js" se encargan de la inyección y análisis de estos, sin que se haya tenido que intervenir manualmente para ello. De esta manera se puede enfocar más tiempo en realmente desarrollar las aplicaciones, en lugar de gestionar estos archivos.

Conclusiones

Por ahora se dejará la aplicación en este punto, en el cual es completamente funcional y aplica varias de las recomendaciones tanto en estructura como en estándares y buenas prácticas para AngularJS 1.x.

La longitud del post actual y el hecho de que falten varios puntos por revisar puede hacer parecer que estas recomendaciones hacen que las aplicaciones web con AngularJS (especialmente las pequeñas como esta) sean más complejas de desarrollar, sin embargo se debe tener en cuenta que:
  • Se tienen muchas explicaciones acerca de las recomendaciones aplicadas.
  • Para demostrar el porqué de varias de estas recomendaciones se han hecho reprocesos, como por ejemplo en el tema de las rutas/estados.
  • Una vez se tengan familiarizadas varias de estas prácticas se podrá ver que son realmente sencillas, inclusive para aplicaciones pequeñas.
  • Son recomendaciones, no procedimientos estrictos. Si para algún proyecto específico otras prácticas resultan más adecuadas se pueden aplicar sin temor a que la aplicación no funcione. Lo que sí es importante es que se tenga consistencia al momento de desarrollar la aplicación.

En un próximo post se espera espera revisar algunas otras de las recomendaciones para proyectos AngularJS 1.x, por lo pronto el proyecto actual se puede descargar desde: https://github.com/guillermo-varela/angularjs-demo/tree/part-I

Referencias

lunes, 22 de febrero de 2016

Tutorial sobre Gulp.js IV: Algunas Prácticas Recomendadas

Introducción

En posts anteriores se ha venido trabajando con Gulp.js, mostrando cómo esta herramienta puede ayudar en la automatización de tareas en los proyectos web, análisis de código JavaScript, actualización automática de cambios en el navegador web e inclusive su posibilidad de integrarse con otras herramientas como Bower.

En esta ocasión se mostrarán un par de prácticas recomendadas al usar Gulp.js que pueden ser de utilidad tanto en proyectos grandes como pequeños.

Para ahorrar algo de tiempo se tomará como base el proyecto desarrollado en el último post introductorio sobre Bower: https://github.com/guillermo-varela/gulp-bower-demo

Nota: en caso de clonar el repositorio desde GitHub, se deben instalar primero las dependencias que ya tiene el proyecto ejecutando:

npm install

Separar las Librerías Externas del Código del Proyecto

Actualmente el equipo de desarrollo de AngularJS está recomendando seguir la guía acerca de estilos y estándares propuestos por John Papahttps://github.com/johnpapa/angular-styleguide

Una de las recomendaciones allí aparecen es separar el código de las librerías externas del código desarrollado para el proyecto.

En el proyecto desarrollado previamente se tiene un archivo ".bowerrc" en el cual se indica que las librerías mediante Bower se descargarán en "app/lib" y es en "app/" donde se encuentra el código del proyecto.

Para seguir esta recomendación se borrará el archivo ".bowerrc", con lo cual al ejecutar "bower install" las librerías se descargarán en la raíz del proyecto en "bower_components/".

Figura 1 - Cambio en la Ubicación de las Librerías de Bower

En ".gitignore" se debe cambiar la entrada "app/lib/" por "bower_components/" para que las dependencias descargadas mediante Bower no se incluyan en el repositorio de código.

También deben hacerse algunos cambios en "gulpfile.js":
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var wiredep     = require('wiredep').stream;
var useref      = require('gulp-useref');
var gulpIf      = require('gulp-if');
var uglify      = require('gulp-uglify');
var gutil       = require('gulp-util');
var cssnano     = require('gulp-cssnano');
var jshint      = require('gulp-jshint');
var jscs        = require('gulp-jscs');
var del         = require('del');
var connect     = require('gulp-connect');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject', 'wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref({ searchPath: ['./', './app'] }))
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src(['**/*.js'], {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src(['**/*.js'], {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del('dist/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(['**/*.css'], {cwd: './app'}, ['inject']);
  gulp.watch(['**/*.js'], {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: './app'}, function(event) {
    gulp.src(event.path)
      .pipe(connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

// Production build
gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Los cambios son:

  • Líneas 21, 25, 59, 67, 80, 81: Dado que ahora las librerías descargadas mediante Bower están en una carpeta por fuera de "app/", ya no es necesario excluir el código de estas librerías de las tareas de Gulp que sólo deben usar el código de la aplicación, como por ejemplo "inject", "jshint", "jscs" y "watch". Esta es una gran ventaja que tiene el seguir esta recomendación ya que la configuración de las tareas de Gulp se hace más sencilla.
  • Línea 35: En la tarea "wiredep" se quita la propiedad "directory: './app/lib/'" ya que la ubicación de las librerías de Bower es la que se usa por defecto. En su lugar se incluye "'ignorePath': '..'" ya que de no hacerlo las referencias en "index.html", al ser URLs relativas tendrían la indicación de carpeta anterior "../", pero debido a que en las tareas "server" y "server-dist" exponen sólo el contenido de la carpeta "app/" (usando gulp-connect) ello generaría errores de recursos no encontrados (404). El resultado sería:
    • ../bower_components/jquery/dist/jquery.js => /bower_components/jquery/dist/jquery.js
    • ../bower_components/bootstrap/dist/js/bootstrap.js  => /bower_components/bootstrap/dist/js/bootstrap.js
  • Línea 43: En la tarea "compress", al usar el plugin "gulp-useref" para concatenar los recursos (JavaScript y CSS) referenciados en "index.html", se incluye la propiedad "searchPath" para indicar que los archivos se deben buscar tanto en la raíz "./" (para encontrar "bower_components") como en "./app".

Al iniciar el servidor de desarrollo, usando el comando "gulp", se podrá ver lo siguiente:
Figura 2 - Aplicación Web sin Dependencias Bower

Al revisar el archivo "index.html" se podrá ver que las dependencias Bower se están indicando con la raíz "/bower_components/" lo cual no existe desde la carpeta "./app/" que se está exponiendo en el servidor web. Para solucionar esto se requiere dejar disponible el contenido de "/bower_components/" como parte de la raíz del servidor web, lo cual se puede hacer usando el plugin "st", el cual expone contenido estático en un servidor web.

Para instalarlo se ejecuta el siguiente comando:

npm install --save-dev st

Para usarlo se debe incluir en "gulpfile.js":

...
var st          = require('st');
...

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

En la tarea de Gulp "server" se le agrega la propiedad "middleware" a "gulp-connect" para que usando "st" se exponga el contenido de "bower_components" como "/bower_components", justo como se tiene en "index.html". Al ejecutar de nuevo "gulp" se puede ver que la página ya carga con los estilos de Bootstrap.
Figura 3 - Aplicación Web con las Dependencias de Bower

Carga Automática de Plugins de Gulp

Hasta este momento al inicio del archivo "gulpfile.js" se tiene la declaración en variables de los plugins de npm que se usan actualmente para las tareas de Gulp del proyecto.

Para hacer que esta sección sea un poco más corta y que por ende el archivo "gulpfile.js" sea más fácil de entender se puede usar el plugin "gulp-load-plugins", el cual carga automáticamente todos los plugins de Gulp declarados en "package.json" y los deja disponible dentro de una variable que se defina. Por ejemplo si se usa la variable "plugins" entonces cada plugin se accede como un método de dicha variable quitando el prefijo "gulp-" y en Camel Case (para los plugins que lleven guión en su nombre), así para usar "gulp-inject" se tendría "plugins.inject".

De esta manera en el archivo "gulpfile.js" se tendría:
'use strict';

var gulp        = require('gulp');
var plugins     = require('gulp-load-plugins')();
var wiredep     = require('wiredep').stream;
var del         = require('del');
var st          = require('st');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(gulp.dest(paths.app));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(plugins.useref({ searchPath: ['./', './app'] }))
    .pipe(plugins.if('**/*.js', plugins.uglify({
      mangle: true
    }).on('error', plugins.util.log)))
    .pipe(plugins.if('**/*.css', plugins.cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src(['**/*.js'], {cwd: './app'})
    .pipe(plugins.jshint())
    .pipe(plugins.jshint.reporter('jshint-stylish'))
    .pipe(plugins.jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src(['**/*.js'], {cwd: './app'})
    .pipe(plugins.jscs())
    .pipe(plugins.jscs.reporter())
    .pipe(plugins.jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del('dist/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(['**/*.css'], {cwd: './app'}, ['inject']);
  gulp.watch(['**/*.js'], {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  pgulp.watch('**/*.html', {cwd: './app'}, function(event) {
    gulp.src(event.path)
      .pipe(plugins.connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  plugins.connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  plugins.connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

// Production build
gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Nota: Los plugins que no son de Gulp (sin prefijo "gulp-") aún deben cargarse explícitamente.

Externalización de Rutas

Para evitar repetir constantemente el mismo conjunto de rutas (globs) y/o archivos, es posible tenerlos en variables aparte o inclusive en un archivo adicional, para que "gulpfile.js" no quede más largo y más fácil de leer y mantener, especialmente si el proyecto tiene una estructura de archivos más compleja (como por ejemplo https://github.com/johnpapa/ng-demos/blob/master/modular/gulp.config.json y https://github.com/johnpapa/ng-demos/blob/master/modular/gulpfile.js).

"gulp-paths.json"
{
  "app": "./app/",
  "js": ["**/*.js"],
  "css": ["**/*.css"],
  "dist": "./dist/"
}

Para los valores que correspondan a carpetas, se recomienda terminar en "/" para mantener consistencia en su uso.

"gulpfile.js"

'use strict';

var gulp        = require('gulp');
var plugins     = require('gulp-load-plugins')();
var paths       = require('./gulp-paths.json');
var wiredep     = require('wiredep').stream;
var del         = require('del');
var st          = require('st');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(gulp.dest(paths.app));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: paths.app})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest(paths.app));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['wiredep'], function () {
  return gulp.src('index.html', {cwd: paths.app})
    .pipe(plugins.useref({ searchPath: ['./', paths.app] }))
    .pipe(plugins.if('**/*.js', plugins.uglify({
      mangle: true
    }).on('error', plugins.util.log)))
    .pipe(plugins.if('**/*.css', plugins.cssnano()))
    .pipe(gulp.dest(paths.dist));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: paths.app})
    .pipe(gulp.dest(paths.dist));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src(paths.js, {cwd: paths.app})
    .pipe(plugins.jshint())
    .pipe(plugins.jshint.reporter('jshint-stylish'))
    .pipe(plugins.jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src(paths.js, {cwd: paths.app})
    .pipe(plugins.jscs())
    .pipe(plugins.jscs.reporter())
    .pipe(plugins.jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del(paths.dist + '**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(paths.css, {cwd: paths.app}, ['inject']);
  gulp.watch(paths.js, {cwd: paths.app}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: paths.app}, function(event) {
    gulp.src(event.path)
      .pipe(plugins.connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  plugins.connect.server({
    root: paths.app,
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  plugins.connect.server({
    root: paths.dist,
    hostname: '0.0.0.0',
    port: 8080
  });
});

// Production build
gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Los cambios fueron:

  • Línea 5: Se carga el archivo "gulp-paths.json" como un objeto JSON en la variable "paths".
  • Líneas 13, 15, 19, 22, 27, 31, 36, 37, 47, 53, 61, 74, 75, 77 y 86: Se reemplazó la cadena "./app" por "paths.app", para usar el valor indicado en "gulp-paths.json".
  • Líneas 15, 53, 61 y 75: Se reemplazó el arreglo "['**/*.js']" por "paths.js", para usar el valor indicado en "gulp-paths.json".
  • Líneas 19 y 74: Se reemplazó el arreglo "['**/*.css']" por "paths.css", para usar el valor indicado en "gulp-paths.json".
  • Líneas 42, 48, 69 y 104: Se reemplazó la cadena "./dist" por "paths.dist", para usar el valor indicado en "gulp-paths.json". Nótese que en la línea 69 se está concatenando "paths.dist" con "'**/*'" para indicar que se debe borrar el contenido y no la carpeta "dist" como tal y en este caso al tener como estándar el finalizar los valores de las carpetas con "/" se sabe que no es necesario incluir este caracter adicional.

Nota: En la tarea "compress" no se reemplazaron las cadenas "**/*.js" ni "**/*.css" ya que en estos casos no se están indicando rutas sino tipos de archivos.

El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/gulp-bower-demo-ii

Referencias

https://github.com/johnpapa/angular-styleguide
https://www.npmjs.com/package/st
https://www.npmjs.com/package/gulp-load-plugins

lunes, 8 de febrero de 2016

Tutorial sobre Bower: Administración de Dependencias Web

https://github.com/bower/bower.github.io/blob/master/img/bower-logo.png

Introducción

Actualmente existen muchos frameworks y librerías disponibles para proyectos web, como por ejemplo AngularJSEmber.jsBackbone.js, Bootstrap, JQuery, entre muchos otros; inclusive se tienen dependencias entre librerías, por ejemplo Bootstrap depende de JQuery.

Cada librería puede incluirse en un proyecto bien sea descargando directamente los archivos necesarios desde su página oficial e incluyéndolos en una carpeta del proyecto o usando referencias hacia un CDN (Content Delivery Network), sin embargo esto hace que se deban incluir y mantener manualmente las dependencias de cada librería.

Bower es una herramienta que permite no solamente descargar librerías para proyectos web sino también tener en cuenta las dependencias que tienen para descargarlas también o advertir que el proyecto tiene una versión de dicha dependencia incompatible. En cierta medida es similar a npm o RubyGems.

Para demostrar su funcionamiento se tomará como base el proyecto desarrollado en el último post introductorio sobre Gulp y se adicionará Bootstrap: https://github.com/guillermo-varela/gulp-demo-watch-live

Nota: en caso de clonar el repositorio desde GitHub, se deben instalar primero las dependencias que ya tiene el proyecto ejecutando:

npm install

Instalación

Bower es un módulo de Node.js, por lo cual se requiere como pre-requisito tener instalado Node.js bien sea con el instalador oficial o mediante Node Version Manager (nvm), como se indica en el post sobre Gulp.

Una vez instalado Node.js se puede instalar Bower globalmente y comprobar la instalación mediante:

npm install -g bower

bower -v
1.7.7

Nota: Para el caso de Bower, no se requiere instalarlo a nivel del proyecto, ya que no se usará su API JavaScript en la construcción del proyecto.

Configuración de Bower

Inicialización

El comando "bower init" permite crear el archivo "bower.json" en el cual se tendrá la configuración de Bower, así como las dependencias usadas. Para ello realizará una serie de preguntas, algunas de las cuales tienen valores por defecto tomadas del archivo "package.json" (si existe) o de la configuración del repositorio Git (si se tiene).

bower init

? name gulp-bower-demo
? description Just a demo project using Bower and Gulp.
? main file
? what types of modules does this package expose?
? keywords bower, bootstrap, gulp
? authors
? license MIT
? homepage https://github.com/guillermo-varela/gulp-bower-demo
? set currently installed components as dependencies? No
? add commonly ignored files to ignore list? No
? would you like to mark this package as private which prevents it from being accidentally published to the registry? Yes

{
  name: 'gulp-bower-demo',
  description: 'Just a demo project using Bower and Gulp.',
  main: '',
  license: 'MIT',
  keywords: [
    'bower',
    'bootstrap',
    'gulp'
  ],
  homepage: 'https://github.com/guillermo-varela/gulp-bower-demo',
  moduleType: [],
  private: true
}

? Looks good? Yes

Al finalizar se debe tener un archivo "bower.json" con el contenido indicado en la confirmación del comando "init".

Carpeta de Instalación de Dependencias

Por defecto las librerías que se instalen como dependencias del proyecto se almacenarán en una carpeta en la raíz del proyecto llamada "bower_components".

Debido a que en este caso el código de la aplicación se encuentra dentro de la carpeta "app" se cambiará la ubicación del directorio de instalación de Bower creando un archivo JSON llamado ".bowerrc" indicando la ruta que se quiere:

{
  "directory": "app/lib"
}

Nota: Para reducir el tamaño ocupado en los repositorios de código, esta carpeta debe ser excluida del sistema de versionamiento.

Administración de Dependencias

Instalando Paquetes

Para instalar una librería como dependencia del proyecto se usa el comando "bower install <paquete>". El paquete a instalar puede ser una URL, un repositorio o un paquete registrado en el sitio oficial de Bower:

# registered package
bower install --save jquery

# GitHub shorthand
bower install --save user/repository

# Git endpoint
bower install --save git://github.com/user/package.git

# URL
bower install --save http://example.com/script.js

El flag "--save" sirve para indicar que adicionalmente a descargar el paquete se quiere también registrar la dependencia en el archivo "bower.json". De esta manera al descargar el proyecto sin las dependencias estas podrán ser descargadas mediante el comando "bower install".

Así al instalar Bootstrap por ejemplo (bower install --save bootstrap) se agrega una nueva entrada en "bower.json" con el nombre "dependencies":

{
  "name": "gulp-bower-demo",
  "description": "Just a demo project using Bower and Gulp.",
  "license": "MIT",
  "keywords": [
    "bower",
    "bootstrap",
    "gulp"
  ],
  "homepage": "https://github.com/guillermo-varela/gulp-bower-demo",
  "moduleType": [],
  "private": true,
  "dependencies": {
    "bootstrap": "^3.3.6"
  }
}

De manera opcional puede indicarse la versión del paquete que se quiere instalar de la siguiente manera: "bower install <paquete>#version". En caso de no indicar la versión Bower instalará la versión más reciente.

Información de los Paquetes

Antes de instalar una dependencia/paquete/librería puede obtenerse información acerca de esta ejecutando "bower info <paquete>" (también se tiene la opción de indicar la versión), por ejemplo para el caso de Bootstrap:

bower info bootstrap

{
  name: 'bootstrap',
  description: 'The most popular front-end framework for developing responsive, mobile first projects on the web.',
  keywords: [
    'css',
    'js',
    'less',
    'mobile-first',
    'responsive',
    'front-end',
    'framework',
    'web'
  ],
  homepage: 'http://getbootstrap.com',
  license: 'MIT',
  moduleType: 'globals',
  main: [
    'less/bootstrap.less',
    'dist/js/bootstrap.js'
  ],
  ignore: [
    '/.*',
    '_config.yml',
    'CNAME',
    'composer.json',
    'CONTRIBUTING.md',
    'docs',
    'js/tests',
    'test-infra'
  ],
  dependencies: {
    jquery: '1.9.1 - 2'
  },
  version: '3.3.6'
}

Available versions:
  - 3.3.6
  - 3.3.5
  - 3.3.4
  - 3.3.2
  - 3.3.1
  - 3.3.0
  - 3.2.0
  - 3.1.1
  - 3.1.0
  - 3.0.3
  - 3.0.2
  - 3.0.1
  - 3.0.0
  - 2.3.2
  - 2.3.1
  - 2.3.0
  - 2.2.2
  - 2.2.1
  - 2.2.0
  - 2.1.1
  - 2.1.0
  - 2.0.4
  - 2.0.3
  - 2.0.2
  - 2.0.1
  - 2.0.0
  - 1.4.0
  - 1.3.0
  - 1.2.0
  - 1.1.1
  - 1.1.0
  - 1.0.0

Show 4 additional prereleases with 'bower info bootstrap --verbose'
You can request info for a specific version with 'bower info bootstrap#'

En la parte resaltada puede verse que para la versión más reciente (3.3.6) se tiene una dependencia con JQuery desde la versión 1.9.1 hasta 2.

Conflicto de Dependencias

Si se intenta instalar Bootstrap pero el proyecto ya está usando una versión distinta de JQuery, Bower preguntará cuál se debe usar:


bower install --save jquery#1.8.9
bower install --save bootstrap

bower bootstrap#*               cached git://github.com/twbs/bootstrap.git#3.3.6
bower bootstrap#*             validate 3.3.6 against git://github.com/twbs/bootstrap.git#*
bower jquery#1.9.1 - 2          cached git://github.com/jquery/jquery-dist.git#2.2.0
bower jquery#1.9.1 - 2        validate 2.2.0 against git://github.com/jquery/jquery-dist.git#1.9.1 - 2

Unable to find a suitable version for jquery, please choose one:
    1) jquery#1.8.0 which resolved to 1.8.0 and is required by gulp-bower-demo
    2) jquery#1.9.1 - 2 which resolved to 2.2.0 and is required by bootstrap#3.3.6

Prefix the choice with ! to persist it to bower.json

? Answer !2
bower jquery                resolution Saved jquery#1.9.1 - 2 as resolution
bower jquery#1.9.1 - 2         install jquery#2.2.0
bower bootstrap#^3.3.6         install bootstrap#3.3.6

jquery#2.2.0 bower_components\jquery

bootstrap#3.3.6 bower_components\bootstrap
└── jquery#2.2.0

En caso de escoger usar la versión compatible con la librería que se quiere instalar, como se hizo en este ejemplo, Bower cambiará la versión que se está usando de la librería anterior por la que necesita para la nueva dependencia en la carpeta de instalación de paquetes, pero en "bower.json" se seguirá indicando que el proyecto necesita la otra versión. Es por esto que lo más recomendable en estos casos es instalar primero la versión de la primera librería que sea compatible con la nueva.

Lista de Paquetes Instalados

El comando "bower list" permite ver los paquetes instalados en el proyecto:

bower list

bower check-new     Checking for new versions of the project dependencies...
gulp-bower-demo /home/user/git/gulp-bower-demo
├─┬ bootstrap#3.3.6 (latest is 4.0.0-alpha.2)
│ └── jquery#2.2.0 (latest is 3.0.0-beta1)
└── jquery#2.2.0 incompatible with 1.8.0 (1.8.0 available, latest is 3.0.0-beta1)

Desinstalar Paquetes

Así como se pueden instalar paquetes, también se pueden desinstalar del proyecto ejecutando "bower uninstall --save <paquete>". Así, para desinstalar JQuery y Bootstrap se debe ejecutar:

bower uninstall --save bootstrap jquery

bower uninstall     bootstrap
bower uninstall     jquery

Otras funcionalidades se pueden encontrar en el API oficial de Bower.

Desarrollo del Proyecto

Como primer paso se debe instalar Bootstrap mediante Bower:

bower install --save bootstrap

Luego se procede a incluir Bootstrap en la página "index.html". Para ese ejemplo se usará como base el ejemplo más sencillo que provee Bootstrap: http://getbootstrap.com/examples/starter-template

app/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp-Bower Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp-Bower Demo</title>

  <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css">

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <script src="lib/jquery/dist/jquery.min.js"></script>
  <script src="lib/bootstrap/dist/js/bootstrap.min.js"></script>

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">

      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Gulp-Bower Demo</a>
      </div>

      <div id="navbar" class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active"><a href="#">Home</a></li>
          <li><a href="#about">About</a></li>
          <li><a href="#contact">Contact</a></li>
        </ul>
      </div>

    </div>
  </nav>

  <div class="container">
    <div class="title">
      <h1>Bootstrap-Gulp-Bower starter template</h1>
      <p class="lead">
        Use this document as a way to quickly start any new project.
      </p>
    </div>

    <span id="first" class="text1"></span>
    <br/>
    <span id="second" class="text2"></span>
  </div>

  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

Los cambios fueron:
  • Línea 13: Se incluye la referencia a los estilos de Bootstrap.
  • Líneas 22-23: Se incluyen las referencias a los archivos JavaScript de JQuery y Bootstrap.
  • Líneas 34-56: Para mostrar el funcionamiento de Bootstrap, se copió el menú superior que se tiene en la página oficial de ejemplo.

En este ejemplo las referencias a los archivos JavaScript de JQuery y Bootstrap se están indicando dentro de la etiqueta "head".

En la documentación de Bootstrap y algunos otros sitios puede encontrarse que los archivos JavaScript se referencian justo antes de cerrar la etiqueta "body", lo cual tiene como origen una recomendación de Yahoo para dar la impresión a los usuarios de una carga más rápida de las páginas web, lo cual se ha transformado en una cuestión de gustos personales, ya que se encuentran opiniones a favor y en contra.

Personalmente, para páginas web que no tengan funcionalidades indispensables en JavaScript estas referencias pueden estar al final, ya que la página se puede mostrar al usuario mientras dichos archivos cargan sin perder mayor funcionalidad. Sin embargo para aplicaciones web que sí dependan de JavaScript para su funcionamiento encuentro que es mejor referenciar los archivos JavaScript al principio, ya que cuando la página se le muestre al usuario se tendrá un sitio funcional, en lugar de botones u opciones que no hacen nada. Este razonamiento se ilustra un poco más en el siguiente artículo: http://demianlabs.com/lab/post/top-or-bottom-of-the-page-where-should-you-load-your-javascript


app/css/style1.css
body {
  padding-top: 50px;
}

.title {
  text-align: center;
}

.text1 {
    color: red;
}

Los cambios fueron:
  • Líneas 1-3: Para que el contenido aparezca bajo el nuevo menú superior, se agrega un espacio de 50 píxeles.
  • Líneas 5-7: Se crea una clase para el título para que el texto esté centrado.

Al abrir el archivo "index.html" en un navegador web puede verse que ya se aplican los estilos de Bootstrap y la funcionalidad del menú tanto en navegadores web de PC como en dispositivos móviles.
Figura 1 - Página en navegador web

Figura 2 - Página en navegador web emulando un iPhone

Integración de Bower con Gulp

Inyección de Archivos

Aprovechando que el proyecto ya está usando Gulp, se mostrará cómo se pueden incluir las referencias a los archivos CSS y JavaScript automáticamente usando el plugin "wiredep", el cual inyecta los archivos declarados en la propiedad "main" de cada paquete y se puede instalar con el siguiente comando:

npm install --save-dev wiredep

app/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp-Bower Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp-Bower Demo</title>

  <!-- build:css css/vendor.min.css -->
  <!-- bower:css -->
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/vendor.min.js -->
  <!-- bower:js -->
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">

      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Gulp-Bower Demo</a>
      </div>

      <div id="navbar" class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active"><a href="#">Home</a></li>
          <li><a href="#about">About</a></li>
          <li><a href="#contact">Contact</a></li>
        </ul>
      </div>

    </div>
  </nav>

  <div class="container">
    <div class="title">
      <h1>Bootstrap-Gulp-Bower starter template</h1>
      <p class="lead">
        Use this document as a way to quickly start any new project.
      </p>
    </div>

    <span id="first" class="text1"></span>
    <br/>
    <span id="second" class="text2"></span>
  </div>

  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

Para que los archivos de los paquetes instalados con Bower se incluyan en "index.html" se realizaron estos cambios:
  • Líneas 13-16: Se agregó el bloque "bower:css" en el cual "wiredep" inyectará los archivos CSS de los paquetes instalados con Bower. Cabe anotar que este bloque se encuentra dentro del bloque "build:css css/vendor.min.css", ya que los archivos que se inyectarán no están minificados, lo cual permitirá trabajar con los archivos sin comprimir en la fase de desarrollo y al momento de construir la versión de producción (tarea "build" de Gulp) estos archivos estarán también minificados en un archivo referenciado antes que los estilos de la aplicación, para permitir sobrescribir los estilos que se necesiten.
  • Líneas 25-28: Similar al punto anterior, se agregó el bloque "bower:js" en el cual "wiredep" inyectará los archivos JavaScript de los paquetes instalados con Bower. También se tiene un bloque "build:js js/vendor.min.js" por aparte, para realizar la minificación sólo para el ambiente de producción.

gulpfile.js
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var wiredep     = require('wiredep').stream;
var useref      = require('gulp-useref');
var gulpIf      = require('gulp-if');
var uglify      = require('gulp-uglify');
var gutil       = require('gulp-util');
var cssnano     = require('gulp-cssnano');
var jshint      = require('gulp-jshint');
var jscs        = require('gulp-jscs');
var del         = require('del');
var connect     = require('gulp-connect');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src(['**/*.js', '!./lib/**/*'], {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src(['**/*.css', '!./lib/**/*'], {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      directory: './app/lib/'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src(['**/*.js', '!./lib/**/*'], {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src(['**/*.js', '!./lib/**/*'], {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del('dist/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(['**/*.css', '!./lib/**/*'], {cwd: './app'}, ['inject']);
  gulp.watch(['**/*.js', '!./lib/**/*'], {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: './app'}, function (event) {
    gulp.src(event.path)
      .pipe(connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

// Production build
gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Las modificaciones fueron:
  • Línea 5: Se carga el módulo "wiredep".
  • Línea 21: En lugar de usar una sola expresión regular (glob) para indicar la ruta de los archivos JavaScript a inyectar mediante "inject", se usa un arreglo para además indicar que se deben ignorar los archivos de los paquetes instalados por Bower, que están en "app/lib".
  • Línea 25: De manera similar al punto anterior, se indica que se deben ignorar los archivos de los paquetes de Bower al inyectar los archivos CSS.
  • Líneas 32-38: Se crea la tarea "wiredep" para realizar la inyección de los archivos de los paquetes instalados por Bower en la carpeta "app/lib" en "app/index.html". Debido a que las tareas "inject" y "wiredep" modificarán el contenido del archivo "app/index.html", para evitar condiciones de carrera se hace que "wiredep" dependa de que "inject" termine de ejecutarse.
  • Línea 41: En la tarea que concatena y minifica los archivos (compress) se adiciona como dependencia la nueva tarea "wiredep" para que asegurar que los archivos de los paquetes instalados mediante Bower se encuentran actualizados en "index.html". En este caso ya no es necesario indicar la tarea "inject" como dependencia de "compress" ya que se tiene en "wiredep".
  • Líneas 59 y 67:  Se excluyen del análisis de código JavaScript los archivos de los paquetes instalados mediante Bower.
  • Líneas 80 y 81: De igual manera se excluyen estos archivos en la tarea de monitoreo "watch".
  • Línea 82: Se crea un paso dentro de la tarea "watch" para que cada vez que se modifique el archivo "bower.json" se ejecute la tarea "wiredep" y así actualizar automáticamente los archivos incluidos en "index.html".
  • Línea 113: Se agregan las tareas "inject" y "wiredep" a la tarea por defecto (default) para que al ejecutar "gulp" antes de iniciar el servidor web de desarrollo (tarea "server") se ejecute la inyección de archivos JavaScript, CSS y dependencias Bower.

En este caso se ha optado por añadir las exclusiones de los archivos en "app/lib" para las tareas de análisis de código (jshint y jscs) y monitoreo de cambios (watch), en lugar de simplemente indicar directamente que se quieren los archivos dentro de "app/js" y "app/css" ya que dependiendo del framework y estructura usados se pueden llegar a tener muchas sub-carpetas que contengan dichos tipos de archivos, por ejemplo puede verse la estructura de archivos que propone Google para los proyectos que usan AngularJS: https://docs.google.com/document/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub

De esta manera, bien sea ejecutando manualmente "gulp wiredep" o instalando las dependencias mediante Bower mientras se ejecuta la tarea de Gulp "watch" (o la tarea por defecto/default que la incluye) se tienen actualizadas las referencias CSS y JavaScript.

Nota: Desde la versión 3.3.5 de Bootstrap se quitó el archivo "dist/css/bootstrap.cs" de la configuración "main", debido a cambios en la documentación de Bower acerca de este campo. Mientras el equipo que trabaja en Bower revisa este tema, se puede adicionar una configuración que permite adicionar nuevamente este archivo para este proyecto:

bower.json
{
  "name": "gulp-bower-demo",
  "description": "Just a demo project using Bower and Gulp.",
  "license": "MIT",
  "keywords": [
    "bower",
    "bootstrap",
    "gulp"
  ],
  "homepage": "https://github.com/guillermo-varela/gulp-bower-demo",
  "moduleType": [],
  "private": true,
  "dependencies": {
    "bootstrap": "^3.3.6"
  },
  "overrides": {
    "bootstrap": {
      "main": [
        "dist/js/bootstrap.js",
        "dist/css/bootstrap.css",
        "less/bootstrap.less"
      ]
    }
  }
}

Líneas 16-24:  Se indica que para este proyecto se sobrescribirá la propiedad "main" para el paquete "bootstrap" usando los archivos necesarios.

Al ejecutar "gulp wiredep" puede verse que se incluyen los archivos tanto de Bootstrap como de JQuery:

index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp-Bower Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp-Bower Demo</title>

  <!-- build:css css/vendor.min.css -->
  <!-- bower:css -->
  <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css" />
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/vendor.min.js -->
  <!-- bower:js -->
  <script src="lib/jquery/dist/jquery.js"></script>
  <script src="lib/bootstrap/dist/js/bootstrap.js"></script>
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>
...

Construcción para Producción

Dado que la tarea "wiredep" ya fue incluida como dependencia de "compress", basta con iniciar la construcción de los archivos para producción para que los archivos de los paquetes instalados mediante Bower sean tenidos en cuenta:

gulp build

Using gulpfile /home/user/git/gulp-bower-demo/gulpfile.js
Starting 'build'...
Starting 'jshint'...
Finished 'jshint' after 110 ms
Starting 'jscs'...
Finished 'jscs' after 248 ms
Starting 'clean:dist'...
Finished 'clean:dist' after 13 ms
Starting 'inject'...
Starting 'wiredep'...
Finished 'wiredep' after 2.4 ms
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 89 ms
Starting 'compress'...
Finished 'compress' after 3.34 s
Starting 'copy:assets'...
Finished 'copy:assets' after 6.51 ms
Finished 'build' after 3.83 s

El resultado en "dist" debe ser:
dist
|   index.html
|
+---assets
|   \---img
|           favicon.ico
|           globe.png
|
+---css
|       styles.min.css
|       vendor.min.css
|
\---js
        scripts.min.js
        vendor.min.js

Ejecutando "gulp" o "gulp server-dist" puede comprobarse que la página funciona perfectamente con el servidor local usando el LiveReload o los archivos de la construcción para producción respectivamente.
Figura 3 - Página desde el servidor local usando archivos de desarrollo "gulp"

Figura 4 - Página desde el servidor local usando archivos de producción "gulp build server-dist"


El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/gulp-bower-demo

Conclusiones

Conociendo de una manera relativamente claro qué es Bower, cómo funciona y cómo se puede integrar con Gulp se puede llegar a tener un ambiente de trabajo para proyectos web más completo, en cuanto que ahora no sólo se tienen tareas automatizadas y análisis de código, sino también administración de dependencias web, lo cual aumenta la probabilidad de encontrar y solucionar problemas antes de desplegar las aplicaciones web en producción.

Referencias