Sondagem de servidor com AngularJS

86

Estou tentando aprender AngularJS. Minha primeira tentativa de obter novos dados a cada segundo funcionou:

'use strict';

function dataCtrl($scope, $http, $timeout) {
    $scope.data = [];

    (function tick() {
        $http.get('api/changingData').success(function (data) {
            $scope.data = data;
            $timeout(tick, 1000);
        });
    })();
};

Quando eu simulo um servidor lento dormindo o thread por 5 segundos, ele aguarda a resposta antes de atualizar a IU e definir outro tempo limite. O problema é quando eu reescrevi o acima para usar módulos Angular e DI para a criação do módulo:

'use strict';

angular.module('datacat', ['dataServices']);

angular.module('dataServices', ['ngResource']).
    factory('Data', function ($resource) {
        return $resource('api/changingData', {}, {
            query: { method: 'GET', params: {}, isArray: true }
        });
    });

function dataCtrl($scope, $timeout, Data) {
    $scope.data = [];

    (function tick() {
        $scope.data = Data.query();
        $timeout(tick, 1000);
    })();
};

Isso só funciona se a resposta do servidor for rápida. Se houver algum atraso, ele envia 1 solicitação por segundo sem esperar por uma resposta e parece limpar a IU. Acho que preciso usar uma função de retorno de chamada. Eu tentei:

var x = Data.get({}, function () { });

mas ocorreu um erro: "Erro: o destino.push não é uma função" Isso foi baseado na documentação de $ resource, mas eu realmente não entendi os exemplos lá.

Como faço a segunda abordagem funcionar?

Dave
fonte

Respostas:

115

Você deve chamar a tickfunção no retorno de chamada para query.

function dataCtrl($scope, $timeout, Data) {
    $scope.data = [];

    (function tick() {
        $scope.data = Data.query(function(){
            $timeout(tick, 1000);
        });
    })();
};
abhaga
fonte
3
Excelente, obrigado. Eu não sabia que você poderia colocar o retorno de chamada lá. Isso resolveu o problema do spam. Também movi a atribuição de dados para dentro do retorno de chamada, o que resolveu o problema de limpeza da IU.
Dave
1
Fico feliz em poder ajudar! Se isso resolveu o problema, você pode aceitar esta resposta para que outros posteriormente também possam se beneficiar dela.
abhaga
1
Supondo que o código acima seja para a páginaA e o controladorA. Como faço para parar esse cronômetro quando navego para a página B e o controladorB?
Varun Verma
6
O processo para parar um $ timeout é explicado aqui docs.angularjs.org/api/ng.$timeout . Basicamente, a função $ timeout retorna uma promessa que você precisa atribuir a uma variável. Em seguida, ouça quando esse controlador for destruído: $ scope. $ On ('destroy', fn ()) ;. Na função de retorno de chamada, chame o método cancel de $ timeout e passe a promessa que você salvou: $ timeout.cancel (timeoutVar); Os documentos de $ interval têm um exemplo melhor ( docs.angularjs.org/api/ng.$interval )
Justin Lucas
1
@JustinLucas, apenas no caso de ser $ scope. $ On ('$ destroy', fn ());
Tomato
33

Versões mais recentes do angular introduziram $ interval que funciona ainda melhor do que $ timeout para polling do servidor.

var refreshData = function() {
    // Assign to scope within callback to avoid data flickering on screen
    Data.query({ someField: $scope.fieldValue }, function(dataElements){
        $scope.data = dataElements;
    });
};

var promise = $interval(refreshData, 1000);

// Cancel interval on page changes
$scope.$on('$destroy', function(){
    if (angular.isDefined(promise)) {
        $interval.cancel(promise);
        promise = undefined;
    }
});
Prumo
fonte
17
-1, não acho que $ intervalo seja adequado, porque você não pode esperar pela resposta do servidor antes de enviar a próxima solicitação. Isso pode resultar em muitas solicitações quando o servidor tem uma latência alta.
Treur
4
@Treur: Embora isso pareça sabedoria convencional hoje em dia, não tenho certeza se concordo. Na maioria dos casos, prefiro uma solução mais resiliente. Considere o caso em que um usuário fica offline temporariamente ou o extremo do seu caso em que o servidor não responde a uma única solicitação. A IU deixará de ser atualizada para usuários de $ timeout, pois um novo tempo limite não será definido. Para usuários de $ interval, a IU retomará de onde parou assim que a conectividade for restaurada. Obviamente, escolher atrasos lógicos também é importante.
Bob
2
Acho que é mais conveniente, mas não resiliente. (Um banheiro no meu quarto também é muito conveniente à noite, mas eventualmente começará a cheirar mal;)) Ao recuperar dados reais usando $ intervalo, você ignora o resultado do servidor. Falta um método para informar o seu usuário, facilitar a integridade dos dados ou, em suma: gerenciar o estado da sua aplicação em geral. No entanto, você pode usar interceptores $ http comuns para isso e cancelar o intervalo $ quando isso acontecer.
Treur
2
Se estiver usando $ q promessas, você pode simplesmente usar o callback finally para garantir que a pesquisa continue, independentemente de a solicitação falhar ou não.
Tyson Nero,
8
Uma alternativa melhor seria lidar não apenas com o evento de sucesso, mas também com o evento de erro. Dessa forma, você pode tentar a solicitação novamente se ela falhar. Você pode até fazer isso em um intervalo diferente ...
Amendoim
5

Aqui está minha versão usando pesquisa recursiva. O que significa que ele aguardará a resposta do servidor antes de iniciar o próximo tempo limite. Além disso, quando ocorrer um erro, ele continuará a votação, mas de uma forma mais tranquila e de acordo com a duração do erro.

A demonstração está aqui

Escreveu mais sobre isso aqui

var app = angular.module('plunker', ['ngAnimate']);

app.controller('MainCtrl', function($scope, $http, $timeout) {

    var loadTime = 1000, //Load the data every second
        errorCount = 0, //Counter for the server errors
        loadPromise; //Pointer to the promise created by the Angular $timout service

    var getData = function() {
        $http.get('http://httpbin.org/delay/1?now=' + Date.now())

        .then(function(res) {
             $scope.data = res.data.args;

              errorCount = 0;
              nextLoad();
        })

        .catch(function(res) {
             $scope.data = 'Server error';
             nextLoad(++errorCount * 2 * loadTime);
        });
    };

     var cancelNextLoad = function() {
         $timeout.cancel(loadPromise);
     };

    var nextLoad = function(mill) {
        mill = mill || loadTime;

        //Always make sure the last timeout is cleared before starting a new one
        cancelNextLoad();
        $timeout(getData, mill);
    };


    //Start polling the data from the server
    getData();


        //Always clear the timeout when the view is destroyed, otherwise it will   keep polling
        $scope.$on('$destroy', function() {
            cancelNextLoad();
        });

        $scope.data = 'Loading...';
   });
guya
fonte
0

Podemos fazer isso facilmente usando o serviço $ interval. aqui está o documento de detalhes sobre $ interval
https://docs.angularjs.org/api/ng/service/$interval O
problema de usar $ interval é que se você estiver fazendo $ http chamada de serviço ou interação com o servidor e se atrasar mais de $ interval time então, antes que sua solicitação seja concluída, ela inicia outra.
Solução:
1. O polling deve ser um status simples obtido do servidor, como um bit único ou json leve, portanto, não deve demorar mais do que o tempo de intervalo definido. Você também deve definir o tempo de intervalo de forma adequada para evitar esse problema.
2. De alguma forma isso ainda está acontecendo por qualquer motivo, você deve verificar um sinalizador global de que a solicitação anterior foi concluída ou não antes de enviar qualquer outra solicitação. Ele perderá esse intervalo de tempo, mas não enviará a solicitação prematuramente.
Além disso, se você quiser definir o valor limite que, após algum valor, a pesquisa deve ser definida, você pode fazer da seguinte maneira.
Aqui está um exemplo de trabalho. explicado em detalhes aqui

angular.module('myApp.view2', ['ngRoute'])
.controller('View2Ctrl', ['$scope', '$timeout', '$interval', '$http', function ($scope, $timeout, $interval, $http) {
    $scope.title = "Test Title";

    $scope.data = [];

    var hasvaluereturnd = true; // Flag to check 
    var thresholdvalue = 20; // interval threshold value

    function poll(interval, callback) {
        return $interval(function () {
            if (hasvaluereturnd) {  //check flag before start new call
                callback(hasvaluereturnd);
            }
            thresholdvalue = thresholdvalue - 1;  //Decrease threshold value 
            if (thresholdvalue == 0) {
                $scope.stopPoll(); // Stop $interval if it reaches to threshold
            }
        }, interval)
    }

    var pollpromise = poll(1000, function () {
        hasvaluereturnd = false;
        //$timeout(function () {  // You can test scenario where server takes more time then interval
        $http.get('http://httpbin.org/get?timeoutKey=timeoutValue').then(
            function (data) {
                hasvaluereturnd = true;  // set Flag to true to start new call
                $scope.data = data;

            },
            function (e) {
                hasvaluereturnd = true; // set Flag to true to start new call
                //You can set false also as per your requirement in case of error
            }
        );
        //}, 2000); 
    });

    // stop interval.
    $scope.stopPoll = function () {
        $interval.cancel(pollpromise);
        thresholdvalue = 0;     //reset all flags. 
        hasvaluereturnd = true;
    }
}]);
user1941574
fonte