wrz
9
2014

AngularJS - generyczna obsługa błędów

W aplikacjach webowych nieodzownym stałym elementem jest obsługa błędów. Dla "normalnej" aplikacji (np. MVC) sprawa jest w miarę prosta, możemy wyjątki przechwycić po stronie serwera, umieścić w modelu (jako model errors) i później na widoku wrzućić w jakieś ValidationSummay. W przypadku aplikacji typu SPA (Single Page Application) sprawa robi się trochę bardziej skomplikowana ponieważ dysponujemy jedną stroną html której zawartość "podmieniamy". Oczywiście możemy przy każdym requeście do serwera sprawdzać kod odpowiedzi i wyświetlać jakiś stały element DIV z odpowiednim komunikatem ale jest to wielce niewygodne, a poza tym kopiowanie tej samej logiki obsługi błędu (nawet wydzielonej do osobnego modułu) nie jest dobrym rozwiązaniem architektonicznym. Jeżeli używamy angular-a jako framework javascriptowy możemy skorzystać z jednego z jego dobrodziejstw i przechwycić response jaki dostajemy ze standardowego modułu angular-resource. Angular-resource jest to moduł ułatwiający komunikację REST-ową z serwerem, możemy tam wysyłać zapytanie typu query (dla listy elementów), get (dla konkretnego elementu), update i delete. Przykład wykorzystania tego modułu:

app.factory('dataService', function ($resource, $filter) {
    return {
        getEventGroups: function () {
            return $resource('/api/Data/GetEventGroups').query();
        }
    };
});

Po stronie serwera mamy zwykłą funkcję WebAPI2 zwracającą IEnumerable<EventGroup>. Otrzymujemy w wyniku obiekt typu $promise który możemy bezpośrednio zwrócić do kontrolera i zbindować do widoku. Ale w przypadku jakiegoś błędu przy komunikacji z serwerem wyświetli się on tylko w konsoli przeglądarki. Jeżeli chcemy go przykadłowo wyświetlić na ekranie musimy go przechwycić. Służy do tego poniższy kod (od razu mówię, nie jest mój - znaleziony gdzieś chyba na StackOverflow):

angular.module('http-error-handling', [])
     .config(function ($provide, $httpProvider, $compileProvider) {

         $httpProvider.responseInterceptors.push(function ($q, logger) {
             return function (promise) {
                 return promise.then(function (successResponse) {
                     return successResponse;
                 },
                 function (errorResponse) {
                     switch (errorResponse.status) {
                         case 404: 
                             logger.logError('Resource not found', errorResponse, 'app', true);
                             break;
                         case 403: 
                             logger.logError( 'Access denied', errorResponse, 'app', true );
                             break;
                         case 500:
                             logger.logError( 'Internal server error', errorResponse, 'app', true );
                             break;
                         default:
                             logger.logError('Other error', errorResponse, 'app', true);
                     }
                     return $q.reject(errorResponse);
                 });
             };
         });
     });

Jak widać możemy obsłużyć osobno poszczególne odpowiedzi HTTP. W kodzie użyty został stworzony customowo serwis logger. Jego kod wygląda tak:

app.factory('logger', function logger($log) {

    var service = {
        getLogFn: getLogFn,
        log: log,
        logError: logError,
        logSuccess: logSuccess,
        logWarning: logWarning
    };

    return service;

    function getLogFn(moduleId, fnName) {
        fnName = fnName || 'log';
        switch (fnName.toLowerCase()) { // convert aliases
            case 'success':
                fnName = 'logSuccess'; break;
            case 'error':
                fnName = 'logError'; break;
            case 'warn':
                fnName = 'logWarning'; break;
            case 'warning':
                fnName = 'logWarning'; break;
        }

        var logFn = service[fnName] || service.log;
        return function (msg, data, showToast) {
            logFn(msg, data, moduleId, (showToast === undefined) ? true : showToast);
        };
    }

    function log(message, data, source, showToast) {
        logIt(message, data, source, true, 'info');
    }

    function logWarning(message, data, source, showToast) {
        logIt(message, data, source, showToast, 'warning');
    }

    function logSuccess(message, data, source, showToast) {
        logIt(message, data, source, showToast, 'success');
    }

    function logError(message, data, source, showToast) {
        logIt(message, data, source, showToast, 'error');
    }

    function logIt(message, data, source, showToast, toastType) {
        var write = (toastType === 'error') ? $log.error : $log.log;
        source = source ? '[' + source + '] ' : '';
        write(source, message, data);
        if (showToast) {
            if (toastType === 'error') {
                toastr.error(message);
            } else if (toastType === 'warning') {
                toastr.warning(message);
            } else if (toastType === 'success') {
                toastr.success(message);
            } else {
                toastr.info(message);
            }
        }
    }
});

Loguje on zarówno do konsoli przeglądarki jak również potrafi wyświetlać komunikaty na ekranie. Do ich wyświetlenia użyta została biblioteka toastr (https://github.com/CodeSeven/toastr)

Na koniec wystarczy wstrzyknąć utworzony moduł do naszego głównego modułu aplikacji:

var app = angular.module('app', ['ngRoute', 'ngResource', 'http-error-handling'])