AngularJS: Где использовать обещания?
Я видел несколько примеров служб входа в Facebook, которые использовали обещания для доступа к FB Graph API.
Пример № 1:
this.api = function(item) {
var deferred = $q.defer();
if (item) {
facebook.FB.api('/' + item, function (result) {
$rootScope.$apply(function () {
if (angular.isUndefined(result.error)) {
deferred.resolve(result);
} else {
deferred.reject(result.error);
}
});
});
}
return deferred.promise;
}
И сервисы, которые использовали "$scope.$digest() // Manual scope evaluation"
когда получил ответ
Пример № 2:
angular.module('HomePageModule', []).factory('facebookConnect', function() {
return new function() {
this.askFacebookForAuthentication = function(fail, success) {
FB.login(function(response) {
if (response.authResponse) {
FB.api('/me', success);
} else {
fail('User cancelled login or did not fully authorize.');
}
});
}
}
});
function ConnectCtrl(facebookConnect, $scope, $resource) {
$scope.user = {}
$scope.error = null;
$scope.registerWithFacebook = function() {
facebookConnect.askFacebookForAuthentication(
function(reason) { // fail
$scope.error = reason;
}, function(user) { // success
$scope.user = user
$scope.$digest() // Manual scope evaluation
});
}
}
Вопросы:
- В чем разница в приведенных выше примерах?
- Каковы причины и случаи использования сервиса $q?
- И как это работает?
4 ответа
Это не будет полным ответом на ваш вопрос, но, надеюсь, это поможет вам и другим, когда вы попытаетесь прочитать документацию по $q
оказание услуг. Мне потребовалось время, чтобы понять это.
Давайте на минуту отложим AngularJS и просто рассмотрим вызовы API Facebook. Оба вызова API используют механизм обратного вызова, чтобы уведомить вызывающего абонента о доступности ответа от Facebook:
facebook.FB.api('/' + item, function (result) {
if (result.error) {
// handle error
} else {
// handle success
}
});
// program continues while request is pending
...
Это стандартный шаблон для обработки асинхронных операций в JavaScript и других языках.
Одна большая проблема с этим шаблоном возникает, когда вам нужно выполнить последовательность асинхронных операций, где каждая последующая операция зависит от результата предыдущей операции. Вот что делает этот код:
FB.login(function(response) {
if (response.authResponse) {
FB.api('/me', success);
} else {
fail('User cancelled login or did not fully authorize.');
}
});
Сначала он пытается войти в систему, а затем, только после проверки успешности входа, он отправляет запрос в Graph API.
Даже в этом случае, который связывает воедино только две операции, все становится запутанным. Метод askFacebookForAuthentication
принимает обратный вызов для неудачи и успеха, но что происходит, когда FB.login
успешно, но FB.api
потерпит неудачу? Этот метод всегда вызывает success
обратный вызов независимо от результата FB.api
метод.
Теперь представьте, что вы пытаетесь закодировать надежную последовательность из трех или более асинхронных операций таким образом, чтобы правильно обрабатывать ошибки на каждом шаге и быть понятными любому другому или даже вам через несколько недель. Возможно, но очень легко просто продолжать вкладывать эти обратные вызовы и пропустить ошибки на этом пути.
Теперь давайте отложим API-интерфейс Facebook на некоторое время и рассмотрим API-интерфейсы Angular Promises, реализованные $q
оказание услуг. Шаблон, реализованный этим сервисом, является попыткой превратить асинхронное программирование обратно во что-то, напоминающее линейную серию простых операторов, с возможностью "выбросить" ошибку на любом этапе пути и обработать ее в конце, семантически аналогично знакомый try/catch
блок.
Рассмотрим этот надуманный пример. Скажем, у нас есть две функции, где вторая функция потребляет результат первой:
var firstFn = function(param) {
// do something with param
return 'firstResult';
};
var secondFn = function(param) {
// do something with param
return 'secondResult';
};
secondFn(firstFn());
Теперь представьте, что firstFn и secondFn требуют много времени для завершения, поэтому мы хотим обработать эту последовательность асинхронно. Сначала мы создаем новый deferred
объект, представляющий цепочку операций:
var deferred = $q.defer();
var promise = deferred.promise;
promise
Свойство представляет конечный результат цепочки. Если вы зарегистрируете обещание сразу после его создания, вы увидите, что это просто пустой объект ({}
). Пока ничего не видно, двигайтесь прямо.
Пока что наше обещание является лишь отправной точкой в цепочке. Теперь давайте добавим две наши операции:
promise = promise.then(firstFn).then(secondFn);
then
Метод добавляет шаг в цепочку, а затем возвращает новое обещание, представляющее конечный результат расширенной цепочки. Вы можете добавить столько шагов, сколько хотите.
Пока что мы создали нашу цепочку функций, но на самом деле ничего не произошло. Вы начинаете дела, звоня deferred.resolve
с указанием начального значения, которое вы хотите передать первому фактическому шагу в цепочке:
deferred.resolve('initial value');
А потом... все равно ничего не происходит. Чтобы гарантировать, что изменения модели соблюдаются должным образом, Angular на самом деле не вызывает первый шаг в цепочке до следующего раза $apply
называется:
deferred.resolve('initial value');
$rootScope.$apply();
// or
$rootScope.$apply(function() {
deferred.resolve('initial value');
});
Так что насчет обработки ошибок? До сих пор мы указывали обработчик успеха только на каждом этапе цепочки. then
также принимает обработчик ошибок в качестве необязательного второго аргумента. Вот еще один, более длинный пример цепочки обещаний, на этот раз с обработкой ошибок:
var firstFn = function(param) {
// do something with param
if (param == 'bad value') {
return $q.reject('invalid value');
} else {
return 'firstResult';
}
};
var secondFn = function(param) {
// do something with param
if (param == 'bad value') {
return $q.reject('invalid value');
} else {
return 'secondResult';
}
};
var thirdFn = function(param) {
// do something with param
return 'thirdResult';
};
var errorFn = function(message) {
// handle error
};
var deferred = $q.defer();
var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);
Как видно из этого примера, каждый обработчик в цепочке имеет возможность перенаправлять трафик на следующий обработчик ошибок, а не на следующий обработчик успеха. В большинстве случаев у вас может быть один обработчик ошибок в конце цепочки, но у вас также могут быть промежуточные обработчики ошибок, которые пытаются восстановить данные.
Чтобы быстро вернуться к вашим примерам (и вашим вопросам), я просто скажу, что они представляют два разных способа адаптации API, ориентированного на обратный вызов Facebook, к Angular-способу наблюдения за изменениями модели. Первый пример оборачивает вызов API в обещание, которое может быть добавлено в область видимости и понимается системой шаблонов Angular. Второй использует более грубый подход - установить результат обратного вызова непосредственно в области, а затем вызвать $scope.$digest()
информировать Angular об изменениях из внешнего источника.
Два примера не сопоставимы напрямую, потому что в первом отсутствует шаг входа в систему. Однако, как правило, желательно инкапсулировать взаимодействия с внешними API-интерфейсами, подобными этим, в отдельных сервисах и предоставлять результаты контроллерам в качестве обещаний. Таким образом вы можете отделить свои контроллеры от внешних проблем и тестировать их с помощью фиктивных сервисов.
Я ожидал сложный ответ, который будет охватывать как: почему они используются в целом и как использовать его в Angular
Это план для угловых обещаний MVP (минимальное жизнеспособное обещание): http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview
Источник:
(для тех, кому лень кликать по ссылкам)
index.html
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-app="myModule" ng-controller="HelloCtrl">
<h1>Messages</h1>
<ul>
<li ng-repeat="message in messages">{{ message }}</li>
</ul>
</body>
</html>
app.js
angular.module('myModule', [])
.factory('HelloWorld', function($q, $timeout) {
var getMessages = function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve(['Hello', 'world']);
}, 2000);
return deferred.promise;
};
return {
getMessages: getMessages
};
})
.controller('HelloCtrl', function($scope, HelloWorld) {
$scope.messages = HelloWorld.getMessages();
});
(Я знаю, что это не решает ваш конкретный пример на Facebook, но я считаю полезными следующие фрагменты)
Через: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/
Обновление от 28 февраля 2014 года. Начиная с версии 1.2.0, обещания больше не разрешаются шаблонами. http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html
(пример плункера использует 1.1.5.)
Отложенный представляет результат асинхронной операции. Он предоставляет интерфейс, который можно использовать для сигнализации о состоянии и результате операции, которую он представляет. Он также предоставляет способ получить связанный экземпляр обещания.
Обещание предоставляет интерфейс для взаимодействия с относящимся к нему отложенным и, следовательно, позволяет заинтересованным сторонам получить доступ к состоянию и результату отложенной операции.
При создании отложенного, его состояние находится в состоянии ожидания и не имеет никакого результата. Когда мы разрешаем () или отклоняем () отложенное, оно меняет свое состояние на разрешенное или отклоненное. Тем не менее, мы можем получить связанное обещание сразу после создания отложенного и даже назначить взаимодействия с его будущим результатом. Эти взаимодействия будут происходить только после того, как отложенное отклонено или разрешено.
Использовать обещание в контроллере и убедиться, что данные доступны или нет
var app = angular.module("app",[]);
app.controller("test",function($scope,$q){
var deferred = $q.defer();
deferred.resolve("Hi");
deferred.promise.then(function(data){
console.log(data);
})
});
angular.bootstrap(document,["app"]);
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
</head>
<body>
<h1>Hello Angular</h1>
<div ng-controller="test">
</div>
</body>
</html>