Polling del server con AngularJS


86

Sto cercando di imparare AngularJS. Il mio primo tentativo di ottenere nuovi dati ogni secondo ha funzionato:

'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 simulo un server lento dormendo il thread per 5 secondi, attende la risposta prima di aggiornare l'interfaccia utente e impostare un altro timeout. Il problema è quando ho riscritto quanto sopra per utilizzare i moduli angolari e DI per la creazione del modulo:

'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);
    })();
};

Funziona solo se la risposta del server è veloce. In caso di ritardo, invia 1 richiesta al secondo senza attendere una risposta e sembra cancellare l'interfaccia utente. Penso di aver bisogno di usare una funzione di callback. Provai:

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

ma ho ricevuto un errore: "Errore: destination.push non è una funzione" Questo era basato sui documenti per $ resource ma non ho davvero capito gli esempi lì.

Come faccio a far funzionare il secondo approccio?

Risposte:


115

Dovresti chiamare la tickfunzione nel callback per query.

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

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

3
Eccellente grazie. Non sapevo che potessi richiamare lì. Questo ha risolto il problema dello spamming. Ho anche spostato l'assegnazione dei dati all'interno del callback che ha risolto il problema di cancellazione dell'interfaccia utente.
Dave

1
Felice di poterti aiutare! Se questo ha risolto il problema, puoi accettare questa risposta in modo che anche altri in seguito possano trarne vantaggio.
abhaga

1
Supponendo che il codice precedente sia per pageA e controllerA. Come si interrompe questo timer quando si accede alla paginaB e al controllerB?
Varun Verma

6
Il processo per fermare un $ timeout è spiegato qui docs.angularjs.org/api/ng.$timeout . Fondamentalmente, la funzione $ timeout restituisce una promessa che devi assegnare a una variabile. Quindi ascolta quando quel controller viene distrutto: $ scope. $ On ('destroy', fn ()) ;. Nella funzione di callback chiama il metodo cancel di $ timeout e passa la promessa che hai salvato: $ timeout.cancel (timeoutVar); I documenti $ interval hanno in realtà un esempio migliore ( docs.angularjs.org/api/ng.$interval )
Justin Lucas

1
@JustinLucas, nel caso in cui dovrebbe essere $ scope. $ On ('$ distruggi', fn ());
Tomato

33

Versioni più recenti di angular hanno introdotto $ interval che funziona anche meglio di $ timeout per il polling del server.

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;
    }
});

17
-1, non penso che $ intervallo sia adatto, perché non puoi aspettare la risposta del server prima di inviare la richiesta successiva. Ciò potrebbe portare a molte richieste quando il server ha un'elevata latenza.
Treur

4
@Treur: Anche se questa sembra essere la saggezza convenzionale di questi tempi, non sono sicuro di essere d'accordo. Nella maggior parte dei casi preferirei una soluzione più resiliente. Considera il caso in cui un utente va temporaneamente offline o l'estremo del tuo caso in cui il server non risponde a una singola richiesta. L'interfaccia utente interromperà l'aggiornamento per gli utenti di $ timeout poiché non verrà impostato un nuovo timeout. Per gli utenti di $ interval, l'interfaccia utente riprenderà da dove era stata interrotta non appena la connettività verrà ripristinata. Ovviamente anche la scelta di ritardi sani è importante.
Bob

2
Penso che sia più conveniente, ma non resiliente. (Un bagno nella mia camera da letto è anche molto comodo di notte, ma alla fine inizierà ad avere un cattivo odore;)) Quando si recuperano i dati effettivi utilizzando $ interval, si ignora il risultato del server. Questo manca di un metodo per informare il tuo utente, facilitare l'integrità dei dati o in breve: gestire lo stato dell'applicazione in generale. Tuttavia, è possibile utilizzare i comuni intercettori $ http per questo e annullare l'intervallo $ quando ciò accade.
Treur

2
Se si utilizzano le promesse $ q, è possibile utilizzare semplicemente il callback finalmente per assicurarsi che il polling continui se la richiesta fallisce o meno.
Tyson Nero

8
Un'alternativa migliore sarebbe quella di gestire non solo l'evento di successo, ma anche l'evento di errore. In questo modo puoi ritentare la richiesta se fallisce. Potresti anche farlo a un intervallo diverso ...
Peanut

5

Ecco la mia versione che utilizza il polling ricorsivo. Ciò significa che attenderà la risposta del server prima di avviare il timeout successivo. Inoltre, quando si verifica un errore, continuerà il polling ma in un maniero più rilassato e in base alla durata dell'errore.

La demo è qui

Scritto di più a riguardo qui

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...';
   });

0

Possiamo farlo facilmente utilizzando il servizio $ interval. ecco il documento di dettaglio su $ interval
https://docs.angularjs.org/api/ng/service/$interval Il
problema con $ interval è che se stai facendo una chiamata al servizio $ http o l'interazione con il server e se in ritardo di più di $ interval time quindi, prima che la tua richiesta sia completata, ne avvia un'altra.
Soluzione:
1. Il polling dovrebbe essere un semplice stato ottenuto dal server come un singolo bit o un json leggero, quindi non dovrebbe richiedere più tempo dell'intervallo definito. È inoltre necessario definire il tempo dell'intervallo in modo appropriato per evitare questo problema.
2. In qualche modo sta ancora accadendo per qualsiasi motivo, dovresti controllare un flag globale che la richiesta precedente è terminata o meno prima di inviare qualsiasi altra richiesta. Mancherà quell'intervallo di tempo ma non invierà la richiesta prematuramente.
Inoltre, se si desidera impostare un valore di soglia che dopo qualche valore deve essere comunque impostato il polling, è possibile farlo in questo modo.
Ecco un esempio funzionante. spiegato in dettaglio qui

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;
    }
}]);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.