AngularJS: Gdzie korzystać z obietnic?


141

Widziałem kilka przykładów usług logowania na Facebooku, które korzystały z obietnic dostępu do FB Graph API.

Przykład 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;
}

I usługi, które były używane, "$scope.$digest() // Manual scope evaluation"gdy otrzymałem odpowiedź

Przykład 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

Oto pytania:

  • Jaka jest różnica w powyższych przykładach?
  • Jakie są powody i przypadki korzystania z usługi $ q ?
  • A jak to działa ?

9
brzmi tak, jakbyś przeczytał, czym są obietnice i dlaczego są one używane w ogóle ... nie dotyczą wyłącznie kątów i jest dużo dostępnych materiałów
charlietfl

1
@charlietfl, słuszna uwaga, ale spodziewałem się złożonej odpowiedzi, która obejmie oba: dlaczego są one ogólnie używane i jak ich używać w Angular. Dzięki za sugestię
Maksym

Odpowiedzi:


401

To nie będzie pełna odpowiedź na twoje pytanie, ale miejmy nadzieję, że pomoże to tobie i innym podczas próby zapoznania się z dokumentacją dotyczącą $qusługi. Zajęło mi trochę czasu, zanim to zrozumiałem.

Odłóżmy na chwilę AngularJS i rozważmy wywołania API Facebooka. Oba wywołania API wykorzystują mechanizm wywołania zwrotnego , aby powiadomić dzwoniącego o dostępności odpowiedzi z Facebooka:

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

Jest to standardowy wzorzec do obsługi operacji asynchronicznych w JavaScript i innych językach.

Jeden duży problem z tym wzorcem pojawia się, gdy trzeba wykonać sekwencję operacji asynchronicznych, w których każda kolejna operacja zależy od wyniku poprzedniej operacji. To właśnie robi ten kod:

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

Najpierw próbuje się zalogować, a dopiero po sprawdzeniu, czy logowanie się powiodło, wysyła żądanie do Graph API.

Nawet w tym przypadku, który polega tylko na połączeniu dwóch operacji w łańcuch, sytuacja zaczyna się komplikować. Metoda askFacebookForAuthenticationakceptuje wywołanie zwrotne w przypadku niepowodzenia i sukcesu, ale co się dzieje, gdy się FB.loginpowiedzie, ale FB.apizawiedzie? Ta metoda zawsze wywołuje successwywołanie zwrotne niezależnie od wyniku FB.apimetody.

Teraz wyobraź sobie, że próbujesz zakodować solidną sekwencję trzech lub więcej operacji asynchronicznych w sposób, który poprawnie obsługuje błędy na każdym kroku i będzie czytelny dla każdego, a nawet dla Ciebie po kilku tygodniach. Możliwe, ale bardzo łatwo jest po prostu zagnieżdżać te wywołania zwrotne i zgubić błędy po drodze.

Teraz odłóżmy na chwilę na bok API Facebooka i po prostu rozważmy API Angular Promises zaimplementowane przez $qusługę. Wzorzec zaimplementowany przez tę usługę jest próbą przekształcenia programowania asynchronicznego z powrotem w coś przypominającego liniową serię prostych instrukcji, z możliwością `` rzucenia '' błędu na dowolnym etapie i obsłużenia go na końcu, semantycznie podobnym do znajomy try/catchblok.

Rozważmy ten wymyślony przykład. Powiedzmy, że mamy dwie funkcje, gdzie druga funkcja zużywa wynik pierwszej:

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

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

 secondFn(firstFn()); 

Teraz wyobraź sobie, że pierwszeFn i secondFn zajmują dużo czasu, więc chcemy przetwarzać tę sekwencję asynchronicznie. Najpierw tworzymy nowy deferredobiekt, który reprezentuje łańcuch operacji:

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

promiseWłaściwość reprezentuje ewentualny wynik łańcuchu. Jeśli zarejestrujesz obietnicę natychmiast po jej utworzeniu, zobaczysz, że jest to po prostu pusty obiekt ( {}). Nie ma jeszcze nic do oglądania, przejdź od razu.

Jak dotąd nasza obietnica reprezentuje jedynie punkt wyjścia w łańcuchu. Teraz dodajmy nasze dwie operacje:

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

thenMetoda dodaje krok do łańcucha, a następnie zwraca nową obietnicę reprezentujący ewentualny wynik rozszerzonego łańcucha. Możesz dodać tyle kroków, ile chcesz.

Do tej pory skonfigurowaliśmy nasz łańcuch funkcji, ale tak naprawdę nic się nie wydarzyło. Rozpoczynasz od wywołania deferred.resolvei określenia wartości początkowej, którą chcesz przekazać do pierwszego rzeczywistego kroku w łańcuchu:

 deferred.resolve('initial value');

A potem ... nadal nic się nie dzieje. Aby upewnić się, że zmiany modelu są prawidłowo obserwowane, Angular nie wywołuje pierwszego kroku w łańcuchu, dopóki nie $applyzostanie wywołany następny raz :

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

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

A co z obsługą błędów? Do tej pory określiliśmy tylko procedurę obsługi sukcesu na każdym etapie łańcucha. thenakceptuje również procedurę obsługi błędów jako opcjonalny drugi argument. Oto kolejny, dłuższy przykład łańcucha obietnic, tym razem z obsługą błędów:

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

Jak widać w tym przykładzie, każdy program obsługi w łańcuchu ma możliwość skierowania ruchu do następnego programu obsługi błędów zamiast do następnego programu obsługi sukcesu . W większości przypadków na końcu łańcucha może znajdować się jedna procedura obsługi błędów, ale można również mieć pośrednie procedury obsługi błędów, które próbują odzyskać.

Aby szybko wrócić do twoich przykładów (i twoich pytań), powiem tylko, że reprezentują one dwa różne sposoby dostosowania API zorientowanego na wywołania zwrotne Facebooka do sposobu obserwowania zmian modelu przez Angular. Pierwszy przykład opakowuje wywołanie API w obietnicę, którą można dodać do zakresu i która jest rozumiana przez system szablonów Angulara. Drugi przyjmuje bardziej brutalne podejście polegające na ustawianiu wyniku wywołania zwrotnego bezpośrednio w zakresie, a następnie wywoływaniu, $scope.$digest()aby Angular był świadomy zmiany z zewnętrznego źródła.

Te dwa przykłady nie są bezpośrednio porównywalne, ponieważ w pierwszym brakuje etapu logowania. Jednak ogólnie pożądane jest hermetyzowanie interakcji z zewnętrznymi interfejsami API, takimi jak ten, w oddzielnych usługach i dostarczanie wyników kontrolerom zgodnie z obietnicą. W ten sposób możesz oddzielić kontrolery od problemów zewnętrznych i łatwiej je przetestować za pomocą usług pozorowanych.


5
Myślę, że to świetna odpowiedź! Dla mnie najważniejsze było opisanie ogólnego przypadku, kiedy obietnica jest naprawdę aktualna. Szczerze mówiąc liczyłem na inny prawdziwy przykład (jak na Facebooku), ale to też chyba działa. Wielkie dzięki!
Maksym

2
Alternatywą dla łączenia wielu thenmetod jest użycie $q.all. Krótki poradnik na ten temat można znaleźć tutaj .
Bogdan

2
$q.alljest odpowiedni, jeśli musisz czekać na zakończenie wielu niezależnych operacji asynchronicznych. Nie zastępuje tworzenia łańcucha, jeśli każda operacja zależy od wyniku poprzedniej operacji.
karlgold

1
zwięźle wyjaśniono tutaj powiązanie ówczesnego łańcucha. Pomógł mi zrozumieć i wykorzystać w pełni jego potencjał. Dzięki
Tushar Joshi

1
Świetna odpowiedź @karlgold! Mam jedno pytanie. Jeśli w ostatnim fragmencie kodu zmienisz return 'firstResult'część na return $q.resolve('firstResult'), jaka będzie różnica?
technophyle

9

Spodziewałem się złożonej odpowiedzi, która obejmie oba: dlaczego są one ogólnie używane i jak ich używać w Angular

To jest podstawa dla kątowych obietnic MVP (minimalna realna obietnica) : http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

Źródło:

(dla osób zbyt leniwych, by klikać linki)

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

  });

(Wiem, że to nie rozwiązuje twojego konkretnego przykładu na Facebooku, ale uważam następujące fragmenty za przydatne)

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


Aktualizacja 28 lutego 2014: od 1.2.0 obietnice nie są już rozwiązywane za pomocą szablonów. http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(przykład plunkera używa 1.1.5.)


afaik kochamy tak, bo jesteśmy leniwi
mkb

to pomogło mi zrozumieć $ q, odroczone i powiązane. Następnie wywołania funkcji, więc dzięki.
aliopi

1

Odroczony reprezentuje wynik operacji asynchronicznej. Udostępnia interfejs, którego można użyć do sygnalizacji stanu i wyniku operacji, którą reprezentuje. Zapewnia również sposób uzyskania powiązanej instancji obietnicy.

Obietnica zapewnia interfejs do interakcji z nią pokrewną odroczoną, a więc umożliwia zainteresowanym stronom uzyskanie dostępu do stanu i wyniku odroczonej operacji.

Podczas tworzenia odroczonego stan jest w toku i nie ma żadnego wyniku. Kiedy rozwiązujemy () lub odrzucamy () odroczony, zmienia on swój stan na rozwiązany lub odrzucony. Mimo to możemy uzyskać powiązaną obietnicę natychmiast po utworzeniu odroczonego, a nawet przypisać interakcje z jej przyszłym wynikiem. Te interakcje wystąpią dopiero po odroczonym odrzuceniu lub rozwiązaniu.


1

skorzystaj z obietnicy w kontrolerze i upewnij się, że dane są dostępne, czy nie

 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>

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.