Инкрементные обновления пользовательского интерфейса из нескольких promises

У меня есть служба AngularJS, которая использовала для получения отдельных контактов (/contacts/:id) на основе индекса (/contacts):

app.service("CollectionService", function($http, $q) {
    this.fetch = function(collectionURL) {
        return $http.get(collectionURL).then(function(response) {
            var urls = response.data;
            var entities = urls.map(function(url) {
                return $http.get(url);
            });
            return $q.all(entities);
        }).then(function(responses) {
            return responses.map(function(response) {
                return response.data;
            });
        });
    };
});

// used within the controller:
CollectionService.fetch("/contacts").then(function(contacts) {
    $scope.contacts = contacts;
});

Результаты отображаются в простом списке (<li ng-repeat="contact in contacts">{{ contact }}</li>).

Однако из-за использования $q.all этот список не обновляется до тех пор, пока не будет получен последний (самый медленный) ответ. Как можно перейти от этого массового обновления к инкрементальным обновлениям при получении отдельных контактов?

Ответ 1

Вы можете использовать свое собственное обещание вернуться, а затем подключиться к уведомлению об обещании дать вам обновление общего хода загрузки и использовать $q.all, чтобы определить, когда это будет завершено. Это в основном то, что у вас есть сейчас с немного другим способом обработки и использования пользовательских обещаний.

Fiddle: http://jsfiddle.net/U4XPU/1/

HTML

<div class="wrapper" ng-app="stackExample">
    <div class="loading" ng-show="loading">Loading</div>
    <div class="contacts" ng-controller="ContactController">
        <div class="contact" ng-repeat="contact in contacts">    {{contact.name}} - {{contact.dob}}</div>
    </div>
</div> 

контроллер

.controller("ContactController", ["$scope", "CollectionService", function (scope, service) {
    scope.contacts = [];
    scope.loading = true;

    service.fetch("/contacts")
        .then(
    // All complete handler
    function () {
        console.log("Loaded all contacts");
        scope.loading = false;
    },
    // Error handler
    function () {
        scope.error = "Ruh roh";
        scope.loading = false;
    },
    // Incremental handler with .notify
    function (newContacts) {
        console.log("New contacts found");
        scope.contacts = scope.contacts.concat(newContacts);
    });
}])

Сервис

.service("CollectionService", ["$q", "$http", function (q, http) {

    this.fetch = function (collectionUrl) {

        var deferred = q.defer();

        http.get(collectionUrl)
        .then(function (response) {

            // Still map all your responses to an array of requests
            var allRequests = response.data.map(function (url) {
                return http.get(url)
                    .then(function (innerResponse) {
                        deferred.notify(innerResponse.data);
                    });
            });

            // I haven't here, but you could still pass all of your data to resolve().
            q.all(allRequests).then(function () {
                deferred.resolve();
            });

        });

        return deferred.promise;

    }

}]);

Вы также можете обрабатывать ошибки, как видите, и .reject() обещание:

http://docs.angularjs.org/api/ng/service/$q

Ответ 2

Вы можете просто передать список контактов в fetch() и отобразить его в списке.

app.service("CollectionService", function($http, $q) {
    this.fetch = function(collectionURL, resultList) {
        $http.get(collectionURL).then(function(response) {
            var urls = response.data;
            urls.forEach(function(url) {
                $http.get(url).then(function(response) {
                    resultList.push(response.data);
                });
            });
        };
    };
});

// used within the controller:
$scope.contacts = [];
CollectionService.fetch("/contacts", $scope.contacts);

Ответ 3

Я придумал обходной путь, разрешающий обратный вызов onResponse, который вызывается для каждого ответа индивидуально:

var entities = urls.map(function(url) {
    var request = $http.get(url);
    if(onResponse) {
        request.then(function(response) {
            onResponse(response.data);
        });
    }
    return response; // this still allows for `$q.all` to handle completion
});

Однако я не увлекаюсь обратными вызовами API-соединения и promises, поэтому мне остается любопытно, есть ли более элегантное решение.

Ответ 4

Может быть, это может сработать, если вы просто вернете пустой список в результате и позвольте службе добавлять записи на каждый запрошенный запрос:

app.service("CollectionService", function($http, $q) {
    this.fetch = function(collectionURL) {
        var list = [];
        $http.get(collectionURL).then(function(response) {
            var urls = response.data;
            urls.forEach(function(url) {
                $http.get(url).then(function(response) {
                    list.push(response.data);
                });
            });
        };
        return list;
    };
});

$scope.contacts = CollectionService.fetch("/contacts");

Ответ 5

Если я правильно понимаю, вам нужно решение, которое должно

  • Начать показ контактов до того, как все контакты будут загружены.
  • Избегайте обратных вызовов
  • Знайте, когда загружены все контакты
  • Убедитесь, что только служба знает о структуре данных
  • Разрешить обработку ошибок в контроллере

app.service("CollectionService", function($http, $q) {
    this.fetch = function(collectionURL) {
        return $http.get(collectionURL).then(function(response) {
            var urls = response.data;
            var contactPromises = urls.map(function(url) {
                return $http.get(url).then(function(response) {
                    return $q.when(response.data);
                });
            });
            return $q.when(contactPromises);
        });
    };
});

// used within the controller:
$scope.contacts = [];
var addContact = function(contact) { $scope.contacts.push(contact); }; 

CollectionService.fetch("/contacts").then(function(contactPromises) {
    contactPromises.forEach(function(contactPromise){
        contactPromise.then(addContact);
    });
    return $q.all(contactPromise);        
}).then(function(){
   alert('All contacts loaded!');
}, function(){
   alert('Error!!');
});