miércoles, 16 de marzo de 2016

Prácticas Recomendadas para Proyectos con AngularJS 1.x Parte III: Manejo de Errores y Documentación


Introducción

En posts anteriores se ha venido desarrollando una aplicación web con AngularJS 1.5, aplicando las recomendaciones que han sido aceptadas por el propio equipo de Google tanto en estructura del proyecto, secciones y rutas como el estilo de desarrollo de componentes como Directivas, Constantes, Controladores y Servicios.

En esta ocasión se van aplicar otras recomendaciones para controlar de manera consistente los errores que se presenten y adicionalmente la generación de la documentación del proyecto.

Manejo de Errores/Excepciones

Errores no Controlados

Debido a la naturaleza dinámica y no tipada de JavaScript es posible que algunos errores se escapen a la verificación de código realizada por JSHint y JSCS. Por ejemplo si al consultar el clima se usa un método que no existe en "$http", al generarse el error no se invoca la función "checkCityError" sino que este aparece en la consola del navegador web.

app/core/weather.service.js
...
      $http.get('http://api.openweathermap.org/data/2.5/weather', {
          params: {
            id: cityId,
            units: 'metric',
            appid: openWheaterAppId
          }
        })
        .thens(checkCityComplete)
        .catch(checkCityError);
...

En este ejemplo se reemplaza la invocación de la función "then" por la inexistente "thens". Al consultar el clima de una ciudad en la aplicación podrá verse el error en la consola.

Figura 1 - Error no controlado

Para tener control sobre este tipo de errores en la aplicación se implementará un mecanismo para detectar este tipo de problemas en la aplicación y es registrar un decorador (decorator) para el servicio "$exceptionHandler" el cual se encarga de procesar los errores/excepciones no capturados. En el nuevo decorador se extenderá la funcionalidad de "$exceptionHandler" con la lógica que se desee.

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

  /**
   * @ngdoc overview
   * @name weatherApp.blocks.exception
   *
   * @description
   * Module for common exception handling.
   */
  angular.module('weatherApp.blocks.exception', []);
})();

app/blocks/exception/exception.handler.decorator.js
(function() {
  'use strict';

  angular
    .module('weatherApp.blocks.exception')
    .config(exceptionConfig);

  /* @ngInject */
  function exceptionConfig($provide) {
    $provide.decorator('$exceptionHandler', extendExceptionHandler);
  }

  /**
   * @ngdoc function
   * @name weatherApp.blocks.exception.function:extendExceptionHandler
   * @private
   *
   * @description
   * Extends the $exceptionHandler service to perform a more advanced AngularJS exception handling.
   *
   * @param {Object} $delegate - $exceptionHandler delegator.
   * @param {Object} $log - Service to log messages.
   * @returns {function} Function defining how to handle the exception.
   */
  /* @ngInject */
  function extendExceptionHandler($delegate, $log) {

    function handleException(exception, cause) {
      $delegate(exception, cause);

      var errorData = {
        exception: exception,
        cause: cause
      };

      var msg = 'Weather Web App Error: ' + exception.message;
      $log.debug(msg, errorData);
    }

    return handleException;
  }
})();

  • Línea 28: Se define la función "handleException" que se invocará cuando se presente un error controlado.
  • Línea 29: Al invocar "$delegate" con los parámetros originales se ejecuta el procesamiento original de "$exceptionHandler".
  • Línea 36-37: Para mantener el ejemplo sencillo, el procesamiento del error será simplemente usar el servicio "$log" para enviar el error a la consola con un mensaje como prefijo.


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',
    'weatherApp.widgets',
    'weatherApp.blocks.exception'
  ]);
})();

Aquí simplemente en la línea 16 se adiciona el nuevo módulo "weatherApp.blocks.exception".

Al ejecutar de nuevo la aplicación se podrá ver que el error ahora aparece con el prefijo indicado.

Figura 2 - Error ahora controlado por la aplicación

Manejo de Excepciones Centralizado

Para tener un manejo consistente de los errores generados en la aplicación se desarrollará un servicio que tome dichos errores y aplique una lógica definida y centralizada.

app/blocks/exception/exception.catcher.service.js
(function() {
  'use strict';

  angular
    .module('weatherApp.blocks.exception')
    .factory('ExceptionCatcher', ExceptionCatcher);

  /**
   * @ngdoc service
   * @name weatherApp.blocks.exception.service:ExceptionCatcher
   *
   * @description
   * Handles exceptions in the application.
   */
  /* @ngInject */
  function ExceptionCatcher($log) {
    var service = {
      catcher: catcher,
      httpCatcher: httpCatcher
    };

    return service;

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

    /**
     * @ngdoc function
     * @name ExceptionCatcher#catcher
     * @methodOf weatherApp.blocks.exception.service:ExceptionCatcher
     *
     * @description
     * Handles an exception with its message.
     *
     * @param {string} event - Name of the event that caused the error.
     * @param {string} message - Message for the exception.
     * @param {object} reason - Object with more information about the error.
     */
    function catcher(event, message, reason) {
      $log.debug(event + ': ' + message, reason);
    }

    /**
     * @ngdoc function
     * @name ExceptionCatcher#httpCatcher
     * @methodOf weatherApp.blocks.exception.service:ExceptionCatcher
     *
     * @description
     * Handles HTTP errors.
     *
     * @param {Object} error - Error object.
     * @param {number} status - HTTP status code.
     * @param {Object} config - Object with the request data.
     */
    function httpCatcher(error, status, config) {
      var message = 'Error: ' + JSON.stringify(error) + '\n' + 'Status: ' + status + '\n';

      if (config && config.headers && config.headers.authorization) {
        config.headers.authorization = config.headers.authorization.replace(/./g, '*');
      }

      if (config.data && config.data.password) {
        config.data.password = config.data.password.replace(/./g, '*');
      }

      if (config.params && config.params.appid) {
        config.params.appid = config.params.appid.replace(/./g, '*');
      }

      catcher('HTTP_ERROR', message, config);
    }
  }
})();
  • Líneas 38-40: Se define la función "catcher" que será invocada cuando se maneje un error en la aplicación. Se tiene un parámetro "event" dado que algunas herramientas de monitoreo y reportes permiten asociar un error a un evento o categoría específico para diferenciar más fácilmente los errores entre sí. De momento esta función sólo envía los errores a la consola, pero una aplicación productiva puede usar servicios como Google AnalyticsFlurry Crash AnalyticsNew Relic, entre otras.
  • Líneas 54-66: Se tiene una función adicional "httpCatcher" para los errores específicos de llamados HTTP, ya que estos cuentan con información adicional (el código HTTP del estado y el objeto "config") y también por motivos de seguridad se deben ofuscar credenciales que puedan hacer parte de las peticiones (reemplazando cada caracter por asterisco). Al final se invoca "catcher" con la información del error debidamente procesada.

app/blocks/exception/exception.handler.decorator.js
(function() {
  'use strict';

  angular
    .module('weatherApp.blocks.exception')
    .config(exceptionConfig);

  /* @ngInject */
  function exceptionConfig($provide) {
    $provide.decorator('$exceptionHandler', extendExceptionHandler);
  }

  /**
   * @ngdoc function
   * @name weatherApp.blocks.exception.function:extendExceptionHandler
   * @private
   *
   * @description
   * Extends the $exceptionHandler service to perform a more advanced AngularJS exception handling.
   *
   * @param {Object} $delegate - $exceptionHandler delegator.
   * @param {Object} ExceptionCatcher - Exception handler.
   * @returns {function} Function defining how to handle the exception.
   */
  /* @ngInject */
  function extendExceptionHandler($delegate, ExceptionCatcher) {

    function handleException(exception, cause) {
      $delegate(exception, cause);

      var errorData = {
        exception: exception,
        cause: cause
      };

      var msg = 'Weather Web App Error: ' + exception.message;
      ExceptionCatcher.catcher('ANGULARJS_ERROR', msg, errorData);
    }

    return handleException;
  }
})();

  • Líneas 22 y 26: Se reemplaza la inyección del servicio "$log" por el nuevo servicio "ExceptionCatcher".
  • Línea 37: Se usa la función "ExceptionCatcher.catcher()" para procesar el error recibido.

Al revisar nuevamente la aplicación podrá verse que el error ya tiene como prefijo el evento "ANGULARJS_ERROR" asignado a los errores no controlados de AngularJS.

Figura 3 - Error con evento como prefijo

Para manejar los errores de las peticiones HTTP se creará un nuevo módulo con un servicio que haga las veces de interceptor.

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

  /**
   * @ngdoc overview
   * @name weatherApp.blocks.http
   *
   * @description
   * Module for common http operations.
   */
  angular.module('weatherApp.blocks.http', []);
})();

app/blocks/http/http.interceptor.service.js
(function() {
  'use strict';

  angular
    .module('weatherApp.blocks.http')
    .factory('HttpInterceptor', HttpInterceptor);

  /**
   * @ngdoc service
   * @name weatherApp.blocks.http.service:HttpInterceptor
   *
   * @description
   * Intercepts all HTTP requests.
   */
  /* @ngInject */
  function HttpInterceptor($q, ExceptionCatcher) {
    var service = {
      responseError: responseError
    };

    return service;

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

    /**
     * @ngdoc function
     * @name HttpInterceptor#responseError
     * @methodOf weatherApp.blocks.http.service:HttpInterceptor
     *
     * @description
     * Handles all HTTP responses with error.
     *
     * @param {Object} rejection - Object with information about the error.
     */
    function responseError(rejection) {
      ExceptionCatcher.httpCatcher(rejection.data, rejection.status, rejection.config);
      return $q.reject(rejection);
    }
  }
})();
  • Línea 35: Se implementa la función "responseError" que será ejecutada automáticamente por AngularJS cuando se presente un error en la respuesta de una petición HTTP.
  • Línea 36: El error obtenido se procesa usando "ExceptionCatcher.httpCatcher()" que como se dijo anteriormente, procesará el error HTTP usando la lógica centralizada para control/reporte de errores.
  • Línea 37: Al final se propaga el error recibido usando "$q.reject()".

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',
    'weatherApp.widgets',
    'weatherApp.blocks.exception',
    'weatherApp.blocks.http'
  ]);
})();

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

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

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

    $httpProvider.interceptors.push('HttpInterceptor');
  }
})();

  • Línea 14: Se adiciona el interceptor en la configuración del módulo principal de la aplicación para que sea usado al fallar cualquier petición HTTP.

Al tenerse el manejo del error de las peticiones HTTP en el interceptor, ya no es necesario enviar a la consola el error capturado en "WeatherService.checkCity.checkCityError", por lo que el servicio quedaría de la siguiente manera:

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

  angular
    .module('weatherApp.core')
    .factory('WeatherService', WeatherService);

  /**
   * @ngdoc service
   * @name weatherApp.core.service:WeatherService
   *
   * @description
   * Service with the weather operations.
   */
  /* @ngInject */
  function WeatherService($q, $http) {
    var openWheaterAppId = 'abc123';

    var service = {
      checkCity: checkCity
    };

    return service;

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

    /**
     * @ngdoc function
     * @name WeatherService#checkCity
     * @methodOf weatherApp.core.service:WeatherService
     *
     * @description
     * Checks the weather on a given city.
     *
     * @param {number} cityId - Id of the city to check.
     * @returns {Promise} Promise that will be resolved with the current weather in the checked city.
     */
    function checkCity(cityId) {
      var deferred = $q.defer();

      $http.get('http://api.openweathermap.org/data/2.5/weather', {
          params: {
            id: cityId,
            units: 'metric',
            appid: openWheaterAppId
          }
        })
        .then(checkCityComplete)
        .catch(checkCityError);

      function checkCityComplete(response) {
        if (!response || !response.data || response.data.cod === '404') {
          checkCityError('City not found');
        } else {
          var cityWeather = {
            name: response.data.name,
            country: response.data.sys.country,
            description: response.data.weather[0].description,
            temperature: response.data.main.temp,
            image: 'http://openweathermap.org/img/w/' + response.data.weather[0].icon + '.png'
          };

          deferred.resolve(cityWeather);
        }
      }

      function checkCityError(error) {
        deferred.reject(error);
      }

      return deferred.promise;
    }
  }
})();

  • Línea 16: Se remueve la inyección del servicio "$window".
  • Líneas 67-69: El error ya no se envía a la consola, simplemente se indica que la promesa retornada originalmente ha generado un error.
Para probar que el interceptor está funcionando se puede indicar un Application ID (variable openWheaterAppId) erróneo y tratar de consultar el clima de una ciudad:

Figura 4 - Error en llamado HTTP controlado con credencial ofuscada

Generación de Documentación

Hasta el momento el código JavaScript se ha estado documentando usando las directivas/etiquetas de "ngdoc". Ahora se usará el plugin "gulp-ngdocs" para generar un sitio web con la documentación del código, similar a las páginas de Javadoc en Java.

npm install gulp-ngdocs --save-dev

gulpfile.js
...
// Generates the documentation
gulp.task('ngdocs', function () {
  var options = {
    title: "Weather Web App Documentation",
    html5Mode: false
  };
  return gulp.src(paths.js, {cwd: paths.app})
    .pipe(plugins.ngdocs.process(options))
    .pipe(gulp.dest('./docs'));
});

// Starts a server with the docs
gulp.task('server-docs', ['ngdocs'], function () {
  plugins.connect.server({
    root: './docs',
    hostname: '0.0.0.0',
    port: 8081
  });
});
...

  • Tarea ngdocs: Busca los archivos JavaScript del proyecto (paths.js) y genera la documentación usando las opciones indicadas en la variable "options", en la que se indica "html5Mode: false" para que la aplicación generada se pueda navegar sin tener que crear reglas adicionales en el servidor. Finalmente almacena esta documentación en la carpeta "docs".
  • Tarea server-docs: Inicia un servidor local luego de ejecutar la tarea "ngdocs" en el cual se puede ver la documentación generada. En este caso se usa el puerto 8081 ya que el 8080 ya está asociado al servidor local de la aplicación (tareas server y server-docs).

.gitignore


dist/
node_modules/
bower_components/
npm-debug.log
docs/

Nota: Este punto depende de las preferencias personales o los estándares de la organización ya que la documentación del proyecto puede generarse automáticamente y hay quienes no gustan tener archivos auto-generados en el repositorio de código, en cuyo caso se debe incluir la carpeta "docs/" en ".gitignore", pero para quienes prefieren tener la documentación siempre disponible en el repositorio pueden obviar esta entrada.


Al ejecutar el comando "gulp server-docs" y abrir la URL "http://localhost:8081" podrá verse la documentación del proyecto.

Figura 5 - Documentación del código del proyecto

Conclusiones

Aunque se trata de elementos relativamente opcionales para el funcionamiento de la aplicación, tener un manejo claro y consistente de los errores y una documentación actualizada permiten entender de manera más rápida y eficiente el funcionamiento actual de la aplicación, las posibles causas de errores que se presenten y su solución de manera más rápida.

En un próximo post se cubrirán algunas de las recomendaciones para generar aplicaciones para producción, por lo pronto el código del proyecto aquí desarrollado se puede descargar desde: https://github.com/guillermo-varela/angularjs-demo/tree/part-III

Referencias

https://docs.angularjs.org/api
https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md

No hay comentarios.:

Publicar un comentario