Promise-ы в AngularJS

от автора

Одной из ключевых составляющих практически любого веб-приложения является взаимодействие с сервером. В больших приложениях это далеко не один запрос. При этом запросы часто необходимо объединять для последовательного или параллельного выполнения, а часто сочетать и то и другое. Кроме того, большие приложения обычно имеют многослойную архитектуру — обертка над RESTFul API => бизнес-сущности => более комплексная бизнес-логика (разбиение условно для примера). И на каждом слое необходимо принять данные в одном формате и передать на следующий слой уже в другом.

Вот со всеми этими задачами могут помочь справиться Promise-ы.

За подробностями добро пожаловать под кат.

Promise-ы предоставляют интерфейс для взаимодействия с объектами, содержащими результат выполнения некоторой операции, время окончания которой неизвестно. Изначально любой promise не разрешен (unresolved) и будет разрешен либо с определенным значением (resolved), либо отвергнут с ошибкой (rejected). Как только promise становится разрешен или отвергнут, его состояние уже не может измениться, что обеспечивает неизменность состояния в течение какого угодно числа проверок. Что не означает, что на разных этапах проверок вы получите одно и тоже значение.

Кроме того, promise-ы можно объединять как для последовательного, так и для параллельного исполнения.

Далее все описание будет построено на базе AngularJS 1.1.5, а все примеры будут исполнены в виде тестов.

Итак, что из себя представляет promise? Это объект с двумя методами:

  • then(successCallback, errorCallback);
  • always(callback);

Что в AngularJS вернет вам promise?

  • $http — сервис для выполнения AJAX-запросов;
  • $timeout — AngularJS-обертка над setTimeout;
  • различные методы $q — сервиса для создания своих deferred-объектов и promise-ов.

Далее последовательно пройдемся по всем вариантам работы с promise-ами.

Простейшее использование

    var responseData;     $http.get('http://api/user').then(function(response){         responseData = response.data;     });  

Ничего интересного — callback и все. Но надо же от чего-то отталкиваться в изложении?.. 🙂

Возврат значения из обработчиков

    var User = function(data){         return angular.extend(this, data);     };     $httpBackend.expectGET('http://api/user').respond(200, {         result: {             data: [{name: 'Artem'}],             page: 1,             total: 10         }     });      var data = {};     $http.get('http://api/user').then(function(response){         var usersInfo = {};          usersInfo.list = _.collect(response.data.result.data, function(u){ return new User(u); });         usersInfo.total = response.data.result.total;          return usersInfo;     }).then(function(usersInfo){         data.users = usersInfo;     }); 

Благодаря такой цепочке then-ов можно строить многослойное приложение. ApiWrapper сделал запрос, выполнил общие обработчики ошибок, отдал данные из ответа без изменений на следующий слой. Там данные преобразовали нужным образом и отдали на следующий. И т.д.

Любое возвращаемое значение из then придет в successcallback следующего then. Не вернули ничего — в success-callback придет undefined (см. тесты).

Чтобы сделать reject — необходимо вернуть $q.reject(value).

    $httpBackend.expectGET('http://api/user').respond(400, {error_code: 11});      var error;     $http.get('http://api/user').then(         null,         function(response){             if (response.data && response.data.error_code == 10){                 return {                     list: [],                     total: 0                 };             }              return $q.reject(response.data ? response.data.error_code : null);         }     ).then(         null,         function(errorCode){             error = errorCode;         }     ); 

Цепочки вызовов

    $httpBackend.expectGET('http://api/user/10').respond(200, {id: 10, name: 'Artem', group_id: 1});     $httpBackend.expectGET('http://api/group/1').respond(200, {id: 1, name: 'Some group'});      var user;     $http.get('http://api/user/10').then(function(response){         user = response.data;         return $http.get('http://api/group/' + user.group_id);     }).then(function(response){         user.group = response.data;     }); 

Это позволит избежать пирамидального кода, сделать код более линейным, а значит более читабельным и простым в поддержке.

Параллельное выполнения запросов с ожиданием всех

$q.all(...) принимает массив или словарь promise-объектов, объединяет их в один, который будет разрешен, когда разрешатся все promise, или отвергнут с ошибкой, когда хотя бы один promise будет отвергнут. При этом значения придут в success-callback либо в виде массива, либо в виде словаря в зависимости от того, как был вызван метод all.

    $httpBackend.expectGET('http://api/obj1').respond(200, {type: 'obj1'})      var obj1, obj2;     var request1 = $http.get('http://api/obj1');     var request2 = $timeout(function(){ return {type: 'obj2'}; });     $q.all([request1, request2]).then(function(values){         obj1 = values[0].data;         obj2 = values[1];     });      expect(obj1).toBeUndefined();     expect(obj2).toBeUndefined();      $httpBackend.flush();     expect(obj1).toBeUndefined();     expect(obj2).toBeUndefined();      $timeout.flush();     expect(obj1).toEqual({type: 'obj1'});     expect(obj2).toEqual({type: 'obj2'}); 
    $q.all({         obj1: $http.get('http://api/obj1'),         obj2: $timeout(function(){ return {type: 'obj2'}; })     }).then(function(values){         obj1 = values.obj1.data;         obj2 = values.obj2;     }); 

$q.when

Обернет любой объект в promise-объект. Чаще всего необходимо для моков в юнит-тестах. Или когда есть цепочка запросов, один из которых нужно выполнять не всегда, а только в некоторых случаях, а в остальных случаях есть уже готовый объект.

    spyOn(UserApi, 'get').andReturn($q.when({id: 1, name: 'Artem'}));      var res;     UserApi.get(1).then(function(user){         res = user;     });      $rootScope.$digest();     expect(res).toEqual({id: 1, name: 'Artem'}); 

Обратите внимание, что для разрешения promise-ов должен быть выполнен хотя бы один $digest цикл.

Создание своих deferred-объектов

$q-сервис также позволяет обернуть любую асинхронную операцию в свой deferred-объект с соответствующим ему promise-объектом.

    var postFile = function(name, file) {         var deferred = $q.defer();          var form = new FormData();         form.append('file', file);          var xhr = new XMLHttpRequest();         xhr.open('POST', apiUrl + name, true);         xhr.onload = function(e) {             if (e.target.status == 200) {                 deferred.resolve();             } else {                 deferred.reject(e.target.status);             }             if (!$rootScope.$$phase) $rootScope.$apply();         };         xhr.send(form);          return deferred.promise;     }; 

Ключевые моменты здесь:

  • создание своего deferred объекта: var deferred = $q.defer()
  • его разрешение в нужный момент: deferred.resolve()
  • или reject в случае ошибки: deferred.reject(e.target.status)
  • возврат связанного promise-объекта: return deferred.promise

Особенности AngularJS

Во-первых, $q-сервис реализован с учетом dirty-checking в AngularJS, что дает более быстрые resolve и reject, убирая ненужные перерисовки браузером.
Во-вторых, при интерполяции и вычислении angular-выражений promise-объекты интерпретируются как значение, полученное после resolve.

    $rootScope.resPromise = $timeout(function(){ return 10; });      var res = $rootScope.$eval('resPromise + 2');     expect(res).toBe(2);      $timeout.flush();     res = $rootScope.$eval('resPromise + 2');      expect(res).toBe(12); 

Это означает, что если следить за изменением переменной через строковый watch, то в качестве значения будет приходить именно resolved-значение.

    var res;     $rootScope.resPromise = $timeout(function(){ return 10; });     $rootScope.$watch('resPromise', function(newVal){         res = newVal;     });      expect(res).toBeUndefined();      $timeout.flush();     expect(res).toBe(10); 

Исключение составляет использование функции. Возвращаемое из нее значение будет использовано как есть, т.е. это будет именно promise-объект.

    $rootScope.resPromise = function(){         return $timeout(function(){ return 10; });     };      var res = $rootScope.$eval('resPromise()');      expect(typeof res.then).toBe('function'); 

Понимание этого важно при написании различных loading-виджетов, loading-директив для кнопок и т.п.

AngularJS 1.2.0 — что нового в promise-ах

  • catch(errorCallback) как укороченный синоним для promise.then(null, errorCallback);
  • для большей схожести с Q finally(callback) переименован в finally(callback);
  • и самое существенное — поддержка нотификации в deferred- и promise-объектах, что удобно использовать для извещения о прогрессе и т.п. Теперь полная сигнатура promise.then выглядит как then(successCallback, errorCallback, progressCallback), а у deferred появился метод notify(progress).

И напоследок скриншот выполненных тестов из WebStorm 7 EAP. Все же приятную добавили интеграцию с karma.

ссылка на оригинал статьи http://habrahabr.ru/post/189084/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *