AngularJS - Como gerenciar requisições assíncronas em Ajax e não perder o fluxo de dados

Se por um lado a tecnologia Ajax de requisições assíncronas foi uma revolução para a Web, permitindo aplicações mais leves, rápidas, robustas e ágeis seu gerenciamento exige cuidados. É muito comum perder dados entre requisições ou então efetuar cálculos ou procedimentos sem ter o cuidado de garantir que os dados requisitados através de um http Post ou Get tenham chegado a tempo.

Talvez o maior segredo para trabalhar com requisições assíncronas é ter sempre em mente que os dados não chegam em uma sequência como a da programação estruturada mas sim na ordem em que as consultas feitas nos web services e fontes de dados são concluídas. Desta forma uma consulta que foi feita primeiro em uma fonte de dados 'A' não necessariamente será concluída antes de uma consulta feita logo depois em uma fonte de dados 'B'. Se o server 'B' for mais rápido que o 'A', por exemplo, a sequência é invertida.

Este cenário é interessante na medida em que não precisamos aguardar 'A' terminar para só depois iniciar 'B', ambos podem ocorrer ao mesmo tempo e a entrega dos dados se dará na sequência em que terminarem, seja esta 'A', 'B' ou 'B','A'.

Este post será dedicado a mostrar uma maneira interessante de controlar estas requisições e garantir que os dados sejam corretamente entregues.

Fazendo do jeito 'errado'


Primeiro vamos ver como é a maneira 'errada', ou seja, sem a garantia de que os dados chegaram corretamente das consultas:


 <!DOCTYPE html>  
 <html ng-app="exemplo"  
 >  
      <head>  
           <script src="https://code.angularjs.org/1.2.3/angular.min.js"></script>  
           <script>  
                var appModule = angular.module('exemplo', []);  
                function GeneralController($scope, $http)  
                {  
                     $scope.campo_A      = "Informação inicial A";  
                     $scope.campo_B      = "Informação inicial B";  
                     var _tmp      = {  
                          texto: "Temporário"  
                     };  
                     $scope.metodoA = function()   
                     {  
                          $scope.ajx(  
                               {  
                                    path: 'fonteA.json'  
                               }  
                          );  
                          $scope.campo_A = _tmp.texto;  
                          console.log('Metodo A: '+$scope.campo_A);  
                     }  
                     $scope.metodoB = function()   
                     {  
                          $scope.ajx(  
                               {  
                                    path: 'fonteB.json'  
                               }  
                          );  
                          $scope.campo_B = _tmp.texto;  
                          console.log('Metodo B: '+$scope.campo_B);  
                     }  
                     $scope.ajx = function(params)  
                     {  
                          $http.post(params.path).success(function(data, status, headers, config)  
                          {  
                            _tmp = data;  
                            console.log("Metodo AJX: "+_tmp.texto);  
                       }).error(function(data, status, headers, config)  
                       {  
                               console.log('error');  
                       });  
                     }  
                     $scope.metodoA();  
                     $scope.metodoB();  
                }  
 </script>  
      </head>  
      <body  
           ng-controller      = "GeneralController"   
      >  
           <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sollicitudin mattis luctus. Proin laoreet ante.</p>  
           <div class="teste">  
                <form>  
                     <input type="text" ng-model="campo_A" >  
                     <input type="text" ng-model="campo_B" >  
                </form>  
           </div>  
      </body>  
 </html>  


Neste exemplo vemos como resposta os textos "Temporário" em ambas as caixas de texto. A string "Temporário" é atribuída a variável temporária "_tmp.texto" (que será nossa variável de transmissão de valores) logo no inicio o script.

Os métodos $scope.metodoA() e $scope.metodoB() são chamados normalmente e as consultas são feitas através do método $scope.ajx(). Atente ao fato de que cada método (A e B) consultam fontes de dados diferentes, respectivamente "fonteA.json" e "fonteB.json".

Porém o resultado não é o esperado como podemos ver no print abaixo

Resposta do script. Ele não conseguiu consultar os jsons externos e retornar os valores corretos.

Podemos também inspecionar os prints dos console.log() que nos mostram qual a sequência em que as variáveis tiveram seus valores atribuídos.
Os valores da consulta ajax chegam atrasados. O ciclo de atribuição do script  é quebrado.


Ou seja, a variável temporária "_tmp.texto" manteve seu valor default e não atualizou o valor oriundo da consulta ajax. Assim $scope.campo_A e $scope.campo_B receberam este valor padrão.

É este o erro mais comum quando trabalhamos com requisições assíncronas, ou seja, as variáveis "perdem o bonde" dos valores pois quando o script diz "agora pegue o novo valor e substitua pelo antigo" este valor novo ainda não existem no ciclo do script pois não chegou a resposta do servidor onde a consulta foi feita.

No print do console é possível ver que os dados vindos da consulta ajax chegaram só mais tarde, depois que a variável temporária já havia preenchido os valores de $scope.campo_A e $scope.campo_B com o valor padrão "Temporário".

Utilizando callbacks


Agora vamos ver como podemos transpor este problema:



 <!DOCTYPE html>  
 <html ng-app="exemplo"  
 >  
      <head>  
           <script src="https://code.angularjs.org/1.2.3/angular.min.js"></script>  
           <script>  
                var appModule = angular.module('exemplo', []);  
                function GeneralController($scope, $http)  
                {  
                     $scope.campo_A      = "Informação inicial A";  
                     $scope.campo_B      = "Informação inicial B";  
                     var _tmp      = {  
                          texto: "Temporário"  
                     };  
                     $scope.funcaoA = function()   
                     {  
                          $scope.ajx(  
                               function(_tmp)  
                               {  
                                    $scope.campo_A = _tmp.texto;  
                                    console.log('Metodo A: '+$scope.campo_A);  
                               },  
                               {  
                                    path: 'fonteA.json'  
                               }  
                          );  
                     }  
                     $scope.funcaoB = function()   
                     {  
                          $scope.ajx(  
                               function(_tmp)  
                               {  
                                    $scope.campo_B = _tmp.texto;  
                                    console.log('Metodo B: '+$scope.campo_B);  
                               },  
                               {  
                                    path: 'fonteB.json'  
                               }  
                          );  
                     }  
                     $scope.ajx = function(successCallbackFn,params)  
                     {  
                          $http.post(params.path).success(function(data, status, headers, config)  
                          {  
                            _tmp = data;  
                            console.log("Metodo AJX: "+_tmp.texto);  
                            successCallbackFn(_tmp);  
                       }).error(function(data, status, headers, config)  
                       {  
                               console.log('error');  
                       });  
                     }  
                     $scope.funcaoA();  
                     $scope.funcaoB();  
                }  
           </script>  
      </head>  
      <body  
           ng-controller      = "GeneralController"   
      >  
           <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sollicitudin mattis luctus. Proin laoreet ante.</p>  
           <div class="teste">  
                <form>  
                     <input type="text" ng-model="campo_A" >  
                     <input type="text" ng-model="campo_B" >  
                </form>  
           </div>  
      </body>  
 </html>  


Os callbacks (no caso successCallbackFn) garantem que após o término da consulta ajax um método será chamado de volta. É este método que garante que o valor retornado da consulta ajax será corretamente atribuído para as variáveis correspondentes.


A função 'function(_tmp){...}' é para onde o callback 'successCallbackFn(_tmp)' será chamado. Dentro de cada método ($scope.metodoA() e $scope.metodoB()) é feita a atribuição correta.

Assim quando $scope.ajx() termina as consultas solicitadas por $scope.metodoA() e $scope.metodoB() os respectivos valores de fonteA.json e fonteB.json serão atribuídos de "_tmp.texto" para $scope.campo_A e $scope.campo_B.

Valores corretos. Textos 'Do json, item A' e 'Do json, item B'


Console com a sequência de atribuição que garante a integridade dos dados. O valor da consulta ajax chega e só depois é feita a atribuição dos valores



Por hoje é isso!


PS: abaixo segue o conteúdo dos aquivos "fonteA.json" e "fonteB.json" caso você deseje reproduzir o exemplo acima:


fonteA.json


 {  
 texto: "Do json, item A"  
 }  


fonteB.json


 {  
 texto: "Do json, item B"  
 }  

Comentários

Postagens mais visitadas deste blog

MySQL - Completando quantidades fixas de caracteres com as funções LPAD() e RPAD()

MySQL - Clonando tabelas na linha de comando

PHP - Gerando arquivo em UTF-8 com fwrite() e utf8_encode()