Přejít k navigační liště

Zdroják » JavaScript » Tvorba moderního e-shopu: zpracování chyb

Tvorba moderního e-shopu: zpracování chyb

Články JavaScript

Efektivní automatizované zpracování chyb v single-page aplikacích? S AngularJS žádný problém. Podíváme se na nejjednodušší implementaci pomocí událostí, řekneme si něco málo o návrhovém vzoru Promise a také se na chvíli zastavíme u response interceptors.

Nálepky:

Úvod

I dnes jsou dostupné všechny zdrojáky na Githubu a můžete si je stáhnout příkazem git checkout -f eshop012.

Zpracování chyb

Začněme nejprve teorií, jak budeme zpracovávat chyby. Se serverem komunikujeme pouze pomocí API. Jakmile na server zašleme požadavek, budeme kontrolovat stavový kód odpovědi. Pokud neproběhla operace úspěšně (především kód 4xx), musíme na to nějakým způsobem zareagovat. Podle různého stavového kódu bude reakce aplikace samozřejmě odlišná:

  • 401 – uživatel je nepřihlášen, tzn. zachytíme všechny odeslané požadavky a zobrazíme formulář pro přihlášení, jakmile se uživatel přihlásí, zašleme předchozí požadavky znova;

  • 403 – uživatel je přihlášen, ale bohužel nemá dostatečná práva k tomu, aby mohl být jeho požadavek zpracován – taktně ho na to upozorníme (tahle chyba by se nikdy neměla stát);

  • 422 – neúspěšná validace nějakého pole (převzato z Github API), zobrazit chybovou hlášku třeba ve vyplňovaném formuláři;

  • 4xx – jakákoliv jiná chyba, zobrazit uživatelsky přívětivou chybovou hlášku.

Chybám s HTTP kódem 401 i 403 se zatím věnovat nebudeme. Vyžadují totiž zvláštní zpracování a autentizaci i autorizaci se budeme věnovat v samostatném díle. Ostatní chyby by bylo ale dobré zpracovat nějak automatizovaně. A právě pro tohle má AngularJS velmi dobrou podporu.

Jak to bude fungovat? V případě neúspěšné operace na straně serveru (např. se pokoušíme vložit uživatele s duplicitním e-mailem) vrátíme chybový kód 4xx. Současně zašleme v odpovědi popis chyby. Na klientské straně pomocí response interceptors zachytíme, že nastala chyba. Následně pomocí metody scope.$broadcast() vyšleme zprávu do aplikace, že došlo k chybě a že je potřeba ji zpracovat. Tuto zprávu zachytí direktiva pro vypisování chybových zpráv a chybu vypíše do šablony. Vše podrobně projdeme v následujícím textu.

Formát chybových zpráv

Abychom mohli chybové zprávy nějak automatizovaně zpracovávat, potřebujeme, aby měly stejnou strukturu. Můžeme se držet nějakého standardizovaného přístupu, např. Google JSON Style Guide pro objekt error. Pro naše potřeby však tento formát lehce upravíme. Budeme vždy zasílat objekt s jednou vlastností errors, která bude obsahovat kolekci, v nichž každý prvek bude reprezentovat právě jednu chybu a u každé chyby budeme mít tyto informace:

  • message – popis chyby pro programátory

  • reason – identifikátor chyby

  • userMessage – popis chyby pro uživatele aplikace

Pokud tedy budeme např. chtít vložit záznam s datem, který bude ve špatném formátu, můžeme vrátit takovou zprávu (předpokládejme, že nebudeme provádět validaci na straně klienta):

{
  "errors": [
    {
      "message": "Datum má chybný formát.",
      "reason": "ValidationError",
      "userMessage": "Zadané datum je uvedeno ve špatném formátu. Zadejte datum ve formátu den.měsíc.rok, např. 13.4.2013."
    }
  ]
}

Pokud dostaneme podobnou zprávu, stačí projít pole chyb, vybrat text vlastnosti userMessage a tyto informace uživateli zobrazit. V našem případě bychom je nejspíš vypsali nad nějakým formulářem. Samozřejmě by bylo možné udělat zpracování chyb ještě použitelnější, mohli bychom ke každé validační chybě vracet ještě další informace, takže bychom pak mohli chybu zobrazit přímo ve formuláři u daného pole ap. Záleží jen na tom, kolik času chceme věnovat tomu, aby byla aplikace co nejpoužitelnější pro uživatele.

Když už známe formát chyb, které budeme dostávat, potřebujeme přijít na to, jak je zpracovat. K tomu však budeme potřebovat znát návrhový vzor Promise. Jeho znalost budeme také potřebovat v dalších dílech seriálu.

Návrhový vzor Promise

V synchronním prostředí se zpracovávají chyby pomocí konstrukce try/catch/finally. To v případě JavaScriptu možné není, podívejme se podrobněji proč.

Řekněme, že chceme v HTML5 zmenšit na polovinu nahraný obrázek. Mohli bychom to provést takto (proměnná data obsahuje obrázek ve formátu base64):

var img = new Image();
img.onload = function() {
  var canvas = document.createElement('canvas');
  var context = canvas.getContext('2d');

  canvas.width = img.width / 2;
  canvas.height = img.height / 2;
  context.drawImage(img, 0, 0, canvas.width, canvas.height);

  var data2 = canvas.toDataURL('image/jpg');
  //v data2 máme zmenšený obrázek.
};
img.src = data;

Jenže co když nastane někde ve funkci obsluhující událost onload chyba? Co kdybychom to uzavřeli do bloku try/catch takto?

try {

  var img = new Image();
  img.onload = function() {
    // … zpracování
    throw Error('Popis nějaké chyby...');
  };
  img.src = data;

} catch (e) {
  //zpracovat chybu...
}

Tohle právě fungovat nebude. Událost onload se totiž vykoná až poté, co kód mimo místo onload v bloku try proběhne. Pokud tedy k vyhození výjimky dojde, nebude zachycena. To je důvod, proč se takto výjimky v JavaScriptu (a tedy i Node.js) používat nemohou.

Právě tuto situaci řeší návrhový vzor Promise. Nejznámější implementace tohoto vzoru je knihovna q, z ní je pak odvozena služba $q v AngularJS.

Služba $q má metodu defer(), která vrací “deferred object”, obsahující dvě metody resolve() a reject(). Metoda resolve() se zavolá po úspěšném zpracování, metoda reject() v případě neúspěchu. Dále deferred object obsahuje odkaz na objekt promise, který má jednu metodu then(). Metoda then() přijímá dva parametry, dva callbacky. První se zavolá v případě úspěšného zpracování, druhý v případě neúspěšného zpracování. Metoda then() vrací další promise.

Slovní popis je poněkud nepřehledný, proto přidám jednoduchou ukázku.

function nejakaAsyncOperace() {
  var deferred = $q.defer();

  //nějaká asynchronní operace, třeba se dotazujeme na API a čekáme 3s na odpověď
  setTimeout(function(){

    //operace proběhla úspěšně, třeba jsme získali hodnotu 123, kterou předáme dále
    $scope.$apply(function(){
      deferred.resolve(123);
    });

    /*kdyby operace neproběhla úspěšně, zavolali bychom metodu reject():
    $scope.$apply(function(){
      deferred.reject('Popis chyby...');
    });
    */

  }, 3000);

  return deferred.promise;
}

var promise = nejakaAsyncOperace();

promise.then(function(){

  //v případě úspěšného zpracování operace bude zavolán tento callback
  console.log('ok');

}, function(){

  //v případě neúspěšného zpracování bude zavolán tento callback  
  console.log('error');

});

Nejprve se bude volat funkce, která je asynchronní. Vrátí proto rovnou objekt promise. Po třech vteřinách bude operace uvnitř funkce hotová, zavolá se tedy metoda resolve() a předá se ji výsledek. Pak se zavolá první callback, který byl předán metodě then(). Pokud se tedy následující kód spustí, po třech vteřinách se v konzoli objeví řetězec “ok”.

Protože metoda then() vrací opět objekt promise, je možné připojit rovnou další metodu then(), jak je vidět na tomto příkladě:

function nejakaAsyncOperace() {
  var deferred = $q.defer();

  setTimeout(function(){
    $scope.$apply(function(){
      deferred.resolve(0);
    });
  }, 3000);
  return deferred.promise;
}

var promise = nejakaAsyncOperace();
promise.then(function(num){
  return num + 1;
}).then(function(num){
  return num + 2;
}).then(function(num){
  console.log(num + 3);
});

V tomto případě se zavolá nejprve první callback a předá se mu jako parametr 0, pak se zavolá druhý callback, který získá jako parametr num hodnotu 0 + 1, na konec se zavolá poslední callback, který získá hodnotu 0 + 1 + 2, přičte 3 a do konzole vypíše 6. V tomto případě metoda then() dostala vždy jen jeden callback, takže jsme možnost chyby neuvažovali.

Pokud potřebujeme uvnitř callbacku oznámit chybu, je potřeba zavolat metodu $q.reject(). Pokud potřebujeme uvnitř callbacku zpracovat opět nějakou asynchronní operaci, vytvoříme nový objekt deferred a vrátíme promise stejně jako uvnitř funkce nejakaAsyncOperace().

Response interceptors

Znalost vzoru Promise byla důležitá proto, abychom pochopili, jak fungují interceptory. Jsou to služby, které budou zavolány po zpracování libovolného HTTP požadavku. Tyto služby přijímají jako parametr instanci promise. Něco zpracují a pošlou zpracování HTTP odpovědi dále. My v tomto bodě budeme z odpovědi získávat informace o chybě a tyto informace pak přepošleme dále k zpracování do šablony.

Interceptor je nejprve potřeba zaregistrovat. To je nutné provést v metodě config(), ve které registrujeme třeba i všechna pravidla pro URL:

module.config(function($httpProvider){
  $httpProvider.responseInterceptors.push('error4xx');
});

Řetězec error4xx je název služby, ve které budeme implementovat zpracování chyb. Služba by mohla vypadat třeba takto:

angular.module('zdrojak.service').factory('error4xx', ['$rootScope', function($rootScope){
  return function(promise)  {
    return promise.then(null, function(res){
      var errors = res.data.errors;
      if (Array.isArray(errors)) {
        var messages = [];
        for (var i = 0; i < errors.length; ++i) {
          if (errors[i].userMessage) {
             messages.push(errors[i].userMessage);
          }
        }
        if (messages.length) {
          $rootScope.$broadcast('messages:add', messages);
        }
      }
      return res;
    });
  };
}]);

V res.data budeme mít objekt odpovědi. Projdeme všechny chyby, pokud jsou zde informace pro uživatele, vložíme je do pole. Následně zavoláme metodu $broadcast(), která byla podrobně popsána v 8. díle.

Direktiva messages

Dále je potřeba vytvořit direktivu, která tuto událost zachytí a vloží do šablony. Direktiva by mohla vypadat třeba takto:

angular.module('zdrojak.directive').directive('messages', function(){
  var template = [
    '<div ng-show="messages" class="alert alert-error">',
    '<p><button class="close" ng-click="closeAlertMessage($index)">×</button></p>',
    '<p ng-repeat="message in messages">{{message}}</p>',
    '</div>'
  ].join("\n");

  var config = {
    scope: {},
    restrict: 'E',
    template: template,
    replace: true,
    link: function(scope, element) {

      scope.$on('messages:add', function(event, messages){
        scope.messages = messages;
      });

      scope.closeAlertMessage = function(index) {
        scope.messages = [];
      }

    }
  };
  return config;
});

Vytváříme tedy nový element messages, který všechny přijaté zprávy vypíše. Nyní stačí do každé šablony vložit nový element do místa, kde chceme chybové zprávy vypisovat, třeba pod nadpis:

<messages></messages>

Jestliže tedy provedeme nějakou událost, nastane chyba, chybu zpracujeme a vypíšeme ji takto do šablony, pak bude výsledek vypadat např. takto:

chybova-hlaska

Flash messenger

Tohle vypadá dobře, ale bylo by lepší přidat ještě jednu službu, která se bude starat o všechny druhy zpráv uživateli, nikoliv pouze chybová oznámení. Vytvořme tedy další službu flash, která přijme ještě typ zasílané zprávy:

angular.module('zdrojak.service').factory('flash', ['$rootScope', function($rootScope){

  function Flash() {}

  Flash.prototype.info = function(message) {
    this.add('info', message);
  };

  Flash.prototype.error = function(message) {
    this.add('error', message); 
  };

  Flash.prototype.add = function(type, message) {
    $rootScope.$broadcast('flashMessages:add', {
      message: Array.isArray(message) ? message : [message],
      type: type
    });  
  };

  Flash.prototype.reset = function() {
    $rootScope.$broadcast('flashMessages:reset');  
  };

  return new Flash();
}]);

Když budeme nyní chtít vypsat do šablony chybovou zprávu, můžeme to provést snadno odkudkoliv tím, že zavoláme metodu flash.error().

Nyní stačí lehce upravit šablonu v direktivě, abychom mohli vypisovat různé druhy chyb.

<div ng-show="messages" class="alert alert-{{messages.type}}">
<p><button class="close" ng-click="closeAlertMessage($index)">×</button></p>
<p ng-repeat="message in messages.message">{{message}}</p>
</div>

Co dále

Pro dnešek je to vše. Za dva týdny definitivně uzavřeme celou administraci a vrhneme se na serverovou část aplikace.

Na tvorbě tohoto článku se svými připomínkami podílel také Pavel Lang. Díky!

Komentáře

Subscribe
Upozornit na
guest
1 Komentář
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
smajl

Díky za výborný seriál o Angularu přímo na praktické téma. Jinak by možná bylo dobré zmínit, že než používat setTimeout a krkolomně obalovat všechno ve $scope.$apply, tak je lepší rovnou sáhnout po $timeout, který vyvolá digest cycle sám.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.