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

7 comentarios:

  1. Hola,
    Estoy siguiendo tus tutoriales de angularJs y estoy teniendo problemas con la tarea de inject en especial con angular-filesort, no consigo que inyecte el app.module.js en primer lugar en el index. ¿Es posible que para poder utilizar esta librería sea necesario tener una estructura del proyecto concreta?

    Un saludo y muchas gracias

    ResponderBorrar
    Respuestas
    1. Hola,
      El plugin gulp-angular-filesort sólo requiere que se le indique la ruta en la que están los archivos Javascript.

      Se tiene problema sólo con el archivo app.module.js?
      Se tiene certeza que el plugin se está ejecutando como parte de la tarea inject?

      Borrar
    2. Hola,
      He comprobado que se realizan los inject en el index, el problema es que no lo hace ordenado. El app.module.js lo mete en el medio y otros módulos tampoco los detecta y no ordena por dependencias.
      Finalmente he optado por esta solución

      gulp.task('inject', function () {

      var services = [];
      services.push('./app/**/*.module.js','./app/**/*.js')
      gulp.src('./app/**/index.html')
      .pipe(inject(gulp.src(services, {read: false})))
      .pipe(gulp.dest('./app'));
      });

      Un saludo y muchas gracias

      Borrar
  2. Tienes que cambiar el componente "gulp-jscs" por que que ya está obsoleto. En su lugar, usa "gulp-eslint" y quita la linea "gulp-jscs": "^3.0.2" de tu archivo package.json. Se puede instalar así:

    npm install gulp-eslint --save-dev

    En el archivo gulpfile.js, la siguiente sección de código:

    // 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'));
    });

    cambialo por:

    // Looks for code style errors in JS and prints them
    gulp.task("jscs", function () {
    return gulp.src(paths.js, {cwd: paths.app})
    .pipe(plugins.eslint({
    "rules":{
    "quotes": [1, "single"],
    "semi": [1, "always"]
    }
    }))
    .pipe(plugins.eslint.format())
    .pipe(plugins.eslint.failOnError());
    });


    Fuente: https://davidwalsh.name/gulp-eslint

    ResponderBorrar
    Respuestas
    1. Hago una adecuación al módulo antes mencionado:

      // Looks for code style errors in JS and prints them
      gulp.task("eslint", function () {
      return gulp.src(paths.js, {cwd: paths.app})
      .pipe(plugins.eslint({ "rules": { "strict": [2, "function"], "camelcase": [2, {"properties": "always"}], "space-infix-ops": 2, "quotes": [2, "double"], "semi": [2, "always"] } }))
      .pipe(plugins.eslint.formatEach())
      .pipe(plugins.eslint.failOnError());
      });

      Borrar
    2. Otro punto muy importante: como buena práctica de programación, los scripts o llamadas de scripts JS deben estar al final del documento HTML y no arriba. Lo único que debe estár arriba, en la etiqueta head son simplemente el archivo de icono y los estilos CSS3 para un mejor rendimiento.

      Borrar
  3. ¿Tiene una curva de aprendizaje alta AngularJS si se enfoca para aplicaciones informaticas ?

    ResponderBorrar