AngularJS: Où utiliser les promesses?

141

J'ai vu quelques exemples de services de connexion Facebook qui utilisaient des promesses pour accéder à l'API FB Graph.

Exemple n ° 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;
}

Et les services qui ont été utilisés "$scope.$digest() // Manual scope evaluation"lors de la réponse

Exemple n ° 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
        });
    }
}

JSFiddle

Les questions sont:

  • Quelle est la différence entre les exemples ci-dessus?
  • Quelles sont les raisons et les cas d'utilisation du service $ q ?
  • Et comment ça marche ?
Maksym
la source
9
on dirait que vous devriez lire ce que sont les promesses et pourquoi elles sont utilisées en général ... elles ne sont pas exclusives à angular et il y a beaucoup de matériel disponible
charlietfl
1
@charlietfl, bon point, mais je m'attendais à une réponse complexe qui couvrira les deux: pourquoi ils sont utilisés en général et comment les utiliser dans Angular. Merci pour votre suggestion
Maksym

Réponses:

401

Cela ne sera pas une réponse complète à votre question, mais j'espère que cela vous aidera, vous et d'autres personnes, lorsque vous essayez de lire la documentation sur le $q service. Il m'a fallu un certain temps pour le comprendre.

Laissons AngularJS de côté pendant un moment et considérons simplement les appels d'API Facebook. Les deux appels d'API utilisent un mécanisme de rappel pour notifier l'appelant lorsque la réponse de Facebook est disponible:

  facebook.FB.api('/' + item, function (result) {
    if (result.error) {
      // handle error
    } else {
      // handle success
    }
  });
  // program continues while request is pending
  ...

Il s'agit d'un modèle standard pour gérer les opérations asynchrones en JavaScript et dans d'autres langages.

Un gros problème avec ce modèle se pose lorsque vous devez effectuer une séquence d'opérations asynchrones, où chaque opération successive dépend du résultat de l'opération précédente. C'est ce que fait ce code:

  FB.login(function(response) {
      if (response.authResponse) {
          FB.api('/me', success);
      } else {
          fail('User cancelled login or did not fully authorize.');
      }
  });

Tout d'abord, il essaie de se connecter, puis ce n'est qu'après avoir vérifié que la connexion a réussi qu'il fait la demande à l'API Graph.

Même dans ce cas, qui n'enchaîne que deux opérations, les choses commencent à se compliquer. La méthode askFacebookForAuthenticationaccepte un rappel en cas d'échec et de réussite, mais que se passe-t-il lorsque FB.loginréussit mais FB.apiéchoue? Cette méthode appelle toujours le successrappel quel que soit le résultat de la FB.apiméthode.

Imaginez maintenant que vous essayez de coder une séquence robuste de trois opérations asynchrones ou plus, d'une manière qui gère correctement les erreurs à chaque étape et sera lisible par n'importe qui d'autre ou même par vous après quelques semaines. Possible, mais il est très facile de continuer à imbriquer ces rappels et de perdre la trace des erreurs en cours de route.

Maintenant, laissons de côté l'API Facebook pendant un moment et considérons simplement l'API Angular Promises, telle qu'implémentée par le $qservice. Le modèle implémenté par ce service est une tentative de transformer la programmation asynchrone en quelque chose qui ressemble à une série linéaire d'instructions simples, avec la possibilité de `` lancer '' une erreur à n'importe quelle étape du processus et de la gérer à la fin, sémantiquement similaire au try/catchbloc familier .

Considérez cet exemple artificiel. Disons que nous avons deux fonctions, où la deuxième fonction consomme le résultat de la première:

 var firstFn = function(param) {
    // do something with param
    return 'firstResult';
 };

 var secondFn = function(param) {
    // do something with param
    return 'secondResult';
 };

 secondFn(firstFn()); 

Imaginez maintenant que firstFn et secondFn prennent tous les deux du temps à se terminer, nous voulons donc traiter cette séquence de manière asynchrone. Nous créons d'abord un nouvel deferredobjet, qui représente une chaîne d'opérations:

 var deferred = $q.defer();
 var promise = deferred.promise;

La promisepropriété représente le résultat final de la chaîne. Si vous enregistrez une promesse immédiatement après l'avoir créée, vous verrez qu'il ne s'agit que d'un objet vide ( {}). Rien à voir encore, avancez tout de suite.

Jusqu'à présent, notre promesse ne représente que le point de départ de la chaîne. Ajoutons maintenant nos deux opérations:

 promise = promise.then(firstFn).then(secondFn);

La thenméthode ajoute une étape à la chaîne, puis retourne une nouvelle promesse représentant le résultat final de la chaîne étendue. Vous pouvez ajouter autant d'étapes que vous le souhaitez.

Jusqu'à présent, nous avons mis en place notre chaîne de fonctions, mais rien ne s'est réellement passé. Vous démarrez les choses en appelant deferred.resolve, en spécifiant la valeur initiale que vous souhaitez passer à la première étape réelle de la chaîne:

 deferred.resolve('initial value');

Et puis ... il ne se passe toujours rien. Pour s'assurer que les changements de modèle sont correctement observés, Angular n'appelle pas réellement la première étape de la chaîne jusqu'à ce que la prochaine fois $applysoit appelée:

 deferred.resolve('initial value');
 $rootScope.$apply();

 // or     
 $rootScope.$apply(function() {
    deferred.resolve('initial value');
 });

Alors qu'en est-il de la gestion des erreurs? Jusqu'à présent, nous n'avons spécifié qu'un gestionnaire de succès à chaque étape de la chaîne. thenaccepte également un gestionnaire d'erreurs comme second argument facultatif. Voici un autre exemple plus long de chaîne de promesses, cette fois avec gestion des erreurs:

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

Comme vous pouvez le voir dans cet exemple, chaque gestionnaire de la chaîne a la possibilité de détourner le trafic vers le prochain gestionnaire d' erreur au lieu du prochain succès gestionnaire de . Dans la plupart des cas, vous pouvez avoir un seul gestionnaire d'erreurs à la fin de la chaîne, mais vous pouvez également avoir des gestionnaires d'erreurs intermédiaires qui tentent de récupérer.

Pour revenir rapidement à vos exemples (et à vos questions), je dirai simplement qu'ils représentent deux façons différentes d'adapter l'API orientée callback de Facebook à la façon dont Angular observe les changements de modèle. Le premier exemple encapsule l'appel d'API dans une promesse, qui peut être ajoutée à une portée et est comprise par le système de modèles d'Angular. Le second adopte l'approche plus brutale consistant à définir le résultat du rappel directement sur la portée, puis à appeler $scope.$digest()pour informer Angular du changement depuis une source externe.

Les deux exemples ne sont pas directement comparables, car le premier ne comprend pas l'étape de connexion. Cependant, il est généralement souhaitable d'encapsuler les interactions avec des API externes comme celle-ci dans des services séparés et de fournir les résultats aux contrôleurs comme des promesses. De cette façon, vous pouvez séparer vos contrôleurs des préoccupations externes et les tester plus facilement avec des services simulés.

Karlgold
la source
5
Je pense que c'est une excellente réponse! L'essentiel, pour moi, était de décrire le cas général où la promesse est vraiment réelle. Honnêtement, j'espérais un autre exemple réel (comme avec Facebook), mais cela fonctionne aussi je suppose. Merci beaucoup!
Maksym
2
Une alternative au chaînage de plusieurs thenméthodes consiste à utiliser $q.all. Un tutoriel rapide à ce sujet peut être trouvé ici .
Bogdan
2
$q.allconvient si vous devez attendre la fin de plusieurs opérations asynchrones indépendantes . Il ne remplace pas le chaînage si chaque opération dépend du résultat de l'opération précédente.
karlgold
1
le chaînage d'alors est expliqué succinctement ici. M'a aidé à le comprendre et à l'utiliser à son plein potentiel. Merci
Tushar Joshi
1
Bonne réponse @karlgold! J'ai une question. Si, dans le dernier extrait de code, vous changez la return 'firstResult'pièce en return $q.resolve('firstResult'), quelle sera la différence?
technophyle
9

Je m'attendais à une réponse complexe qui couvrira les deux: pourquoi ils sont utilisés en général et comment les utiliser dans Angular

C'est le but des promesses angulaires MVP (promesse viable minimum) : http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

La source:

(pour ceux qui sont trop paresseux pour cliquer sur les liens)

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

  });

(Je sais que cela ne résout pas votre exemple Facebook spécifique, mais je trouve les extraits suivants utiles)

Via: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/


Mise à jour du 28 février 2014: à partir de la version 1.2.0, les promesses ne sont plus résolues par les modèles. http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(L'exemple plunker utilise 1.1.5.)

Mars Robertson
la source
afaik nous aimons tellement parce que nous sommes paresseux
mkb
cela m'a aidé à comprendre $ q, les appels de fonction différés et enchaînés .then, alors merci.
aliopi
1

Un différé représente le résultat d'une opération asynchrone. Il expose une interface qui peut être utilisée pour signaler l'état et le résultat de l'opération qu'il représente. Il fournit également un moyen d'obtenir l'instance de promesse associée.

Une promesse fournit une interface pour interagir avec sa relation différée, et permet ainsi aux parties intéressées d'accéder à l'état et au résultat de l'opération différée.

Lors de la création d'un différé, son état est en attente et il n'a aucun résultat. Lorsque nous résolvons () ou rejetons () le différé, il change son état en résolu ou rejeté. Néanmoins, nous pouvons obtenir la promesse associée immédiatement après la création d'un différé et même attribuer des interactions avec son résultat futur. Ces interactions n'auront lieu qu'après le rejet ou la résolution du différé.

Bélier G
la source
1

utiliser la promesse au sein d'un contrôleur et s'assurer que les données sont disponibles ou non

 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>

Manivannan A
la source