jQuery Promise. Как заставить промисы работать на себя
Промисы, обещания, ад из колбэков, объект Deferred, then-ы, when-ы и резолвы...
Эти слова доносятся из каждого телеграфного столба. Есть ощущение, что я последний оболтус на этой планете, который не пользуется промисами. Погрустив на сей счет, я затеял разобраться с этой темой. Но как и в случае с git rebase выяснилось, что в интернетах много информации о промисах, но мало где есть объяснение на пальцах. А раз в интернетах чего-то нет, надо создать это самому.
Меня мало интересует общая теория и тонкости этой замечательной штуки. Любую незнакомую вещь я воспринимаю с точки зрения возможной пользы от нее. Поэтому подробные объяснения механизма работы промисов лучше поискать в других блогах. Мы же с вами рассмотрим пример практической работы с jquery promise. И в итоге выясним, стоит ли разбираться с этим подробнее или же можно дальше законно писать тонны кода в $.ajax.success.
Когда нам вообще могут понадобиться промисы?
Типичный пример. Мы разбираемся в новом проекте, где добрые бэкендщики напилили кучу мелких апишных запросов, выполняющих строго определенные действия. Один запрос - одна небольшая операция. В общем, так и должно быть при построении REST-сервиса. Но пока оставим понты про REST и спустимся на землю.
В нашем воображаемом проекте есть сущность Пользователь, по-программистски user, и Заказ - order. И есть несколько get и post-запросов, выполняющих такие операции:
- 1. GET php/user/info.php - получение информации о пользователе
- 2. GET php/user/orders.php - получение заказов пользователя
- 3. POST php/user/new.php - добавление нового пользователя
- 4. POST php/order/new.php - добавление заказа
- 5. POST php/order/applyBonus.php - применение бонуса к заказу (списание некой суммы с определенного заказа)
Это простые запросы, которые делают несложные манипуляции с базой данных. А на фронте у нас есть набор разнообразных задач, которые сводятся к выполнению определенных запросов, ожиданию ответа от них, парсингу и передаче оного ответа в следующий запрос.
Даже если нам нужно отправить один запрос и в success выполнить некий код - уже выглядит не очень хорошо. А если взять результат первого запроса и отправить его во второй? А если нужно последовательно выполнить 2 запроса, дождаться получения данных, эти данные использовать в третьем, и только после этого выполнить какие-то действия? Ужас полный.
Вы спросите: что же это за дикие требования, где нужно последовательно выполнить 3 ajax-запроса и только потом совершить какое-то полезное действие? А сейчас мы набросаем десяток задач, для решения которых задействуем промисы. Задачи будут усложняться и последней выйдет эта самая тема из трех последовательных запросов. И самое главное, мы напишем код, который позволит вменяемо управлять любыми комбинациями ajax-запросов. Приступим.
Какие задачи мы будем решать?
Оглашаю весь список:
- 1. Простое получение информации о пользователе - 1 get-запрос
- 2. Получение списка заказов пользователя - 1 get-запрос
- 3. Получение последнего заказа - 1 get-запрос + обработка данных на клиенте
- 4. Общая сумма всех заказов пользователя - 1 get-запрос + обработка данных на клиенте
- 5. Добавление пользователя - 1 post-запрос
- 6. Добавление заказа - 1 post-запрос
- 7. Добавление заказа с регистрацией пользователя - 2 post-запроса. Второй запрос использует результаты первого.
- 8. Применение бонуса к заказу - 1 post-запрос
- 9. Применение бонуса к последнему заказу пользователя - 2 get и 1 post-запрос
Как видим, большинство задач великой сложности не представляют. Их вполне можно решить и обычными $.ajax.success колбэками. Но во-первых, Ваш код будет быстро захламлен, во-вторых, его сложно будет переиспользовать, а в-третьих, только представьте, какую портянку придется написать для решения задачи номер 9 с тремя последовательными запросами.
Усложнять задачи мы будем плавно, чтобы привыкнуть к возможно необычному javascript-коду. Интерфейсно выглядит так: на каждую из вышеозначенных задач мы создадим по одной кнопке. Клик на кнопку запустит нужную последовательность запросов, сответствующую номеру задачи.
Конечно, не особо эффектно, но нам нужно не навести красоту, а понять, как заставить промисы работать на наше с вами благо. Пафосный заголовок статьи вполне соответствует ее содержанию. Мы напишем несколько js-модулей, которые позволят выполнять и комбинировать какие угодно задачи, используя всего лишь 5 апи-запросов к серверу. Но для начала этот серверный код надобно написать.
Пишем серверный код. Конечно же, PHP
Набросаем на скорую руку 5 php-скриптов. А заодно разберем, какие параметры они будут принимать и что возвращать. В корне проекта создадим папку php, а в ней еще 2 - user и order. И по порядку рассмотрим все файлы-запросы.
PHP. Получение информации о пользователе
GET /php/user/info.php
if ($_SERVER['REQUEST_METHOD'] === 'GET') { // Принимаем данные из массива $_GET $userId = (int)$_GET['userId']; // Вытаскиваем информацию о пользователе из базы... sleep(1); // Возвращаем email и бонус пользователя echo json_encode(array( 'email' => 'webdevkin@gmail.com', 'bonus' => rand(500, 1000) )); } else { header('HTTP/1.0 405 Method Not Allowed'); echo json_encode(array( 'error' => 'Метод не поддерживается' )); }
Принимаем на вход параметр userId, затем ждем 1 секунду (имитируем какую-то полезную работу) и возвращаем email и бонус пользователя в json-объекте. Сгенерируем бонус рандомом, чтобы было не так грустно следить за одними и теми же числами.
Есть один момент: если попытаемся обратиться к скрипту не GET-запросом, то вернем http-ошибку 405 с соответствующим текстом в поле error. Пока не обращайте внимания, вспомним об этом в конце статьи.
Конечно, в реале у Вас будет написан код, лезущий в базу, но этот пост не о том. Сейчас нам достаточно лишь сымитировать запросы, уделив немного внимания входным параметрам и формату ответов от сервера. Все это пригодится при написании js-кода с промисами. Оставшиеся 4 апи-запроса мы напишем по аналогии.
PHP. Список заказов пользователя
GET /php/user/orders.php
if ($_SERVER['REQUEST_METHOD'] === 'GET') { // Принимаем данные из массива $_GET $userId = (int)$_GET['userId']; // Вытаскиваем информацию о пользователе из базы... sleep(1); // Возвращаем заказы пользователя echo json_encode(array( 'orders' => array( array('orderId' => rand(1, 5), 'summa' => rand(1000, 1500)), array('orderId' => rand(10, 20), 'summa' => rand(2000, 5000)), array('orderId' => rand(30, 50), 'summa' => rand(10000, 20000)) ) )); } else { header('HTTP/1.0 405 Method Not Allowed'); echo json_encode(array( 'error' => 'Метод не поддерживается' )); }
Принимаем userId, отдаем массив из объектов с двумя полями: id и сумма заказа.
PHP. Создание пользователя
POST /php/user/new.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Принимаем данные из массива $_POST $email = $_POST['email']; $name = $_POST['name']; // Типа добавляем пользователя в базу... sleep(1); // Возвращаем id созданного пользователя, для примера - random echo json_encode(array( 'userId' => rand(1, 100) )); } else { header('HTTP/1.0 405 Method Not Allowed'); echo json_encode(array( 'error' => 'Метод не поддерживается' )); }
На входе из массива $_POST берем email и имя, возвращаем id созданного пользователя.
PHP. Добавление заказа
POST /php/order/new.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Принимаем данные из массива $_POST $userId = (int)$_POST['userId']; $summa = (int)$_POST['summa']; // Добавляем заказ в базу... sleep(1); // Возвращаем id созданного заказа, для примера - random echo json_encode(array( 'orderId' => rand(1, 1000) )); } else { header('HTTP/1.0 405 Method Not Allowed'); echo json_encode(array( 'error' => 'Метод не поддерживается' )); }
Принимаем id пользователя и сумму заказа, возвращаем id созданного заказа.
PHP. Применение бонуса к заказу
POST /php/order/applyBonus.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Принимаем данные из массива $_POST $orderId = (int)$_POST['orderId']; $bonus = (int)$_POST['bonus']; // Добавляем заказ в базу... sleep(1); // Возвращаем код успеха echo json_encode(array( 'code' => 'success' )); } else { header('HTTP/1.0 405 Method Not Allowed'); echo json_encode(array( 'error' => 'Метод не поддерживается' )); }
На вход - id заказа и сумма бонуса, на выходе просто код успеха, мол, все хорошо прошло.
С подготовкой закончили, переходим к клиентской части и js-коду.
Каркас проекта и html-заготовка
Создадим файл index.html в корне проекта. В секции head напишем такое
Promise jQuery
Немного стилей прямо в секции head - пусть будет так. В body поместим следующее.
Promise jQuery
Здесь видим 9 кнопок, по клику на которые запускаются вышеозначенные операции. С каждым разом задачи будут усложняться. Реализовывать функционал мы будем последовательно. А в конце с удивлением заметим, что написали функцию, которая дождалась выполнения двух ajax-запросов и использовала их результаты в третьем, а после завершения третьего сделала что-то еще... Звучит тяжко, но это окажется не сложнее, чем отправить один-единственный get-запрос за данными. Объем кода для всех 9-ти задач будет примерно одинаковый - по десятку простых строк.
В конце index.html подключены 4 js-файла. jquery закиньте в папку js сразу, файлы user.js и order.js пока оставьте пустыми. А с main.js мы немного поработаем и напишем базовый код инициализации нашего приложения.
Заготовка main.js
Общая структура файла такова
// Главный модуль приложения 'use strict'; var app = (function() { var userId = Math.round(Math.random() * 100); // Получаем информацию о пользователе function _userInfo() { // ... } // Получаем список заказов пользователя function _userOrders() { // ... } // Последний заказ пользователя function _userLastOrder() { // ... } // Общая сумма всех заказов function _userTotalSummaOrders() { // ... } // Добавление пользователя function _userNew() { // ... } // Добавление заказа function _orderNew() { // ... } // Добавление заказа с созданием пользователя function _orderNewWithUser() { // ... } // Применение бонуса к заказу function _orderApplyBonus() { // ... } // Применение бонуса к последнему заказу пользователя function _userApplyBonusToLastOrder() { // ... } // Ловим ошибки в промисах function onCatchError(response) { // ... } // Навешиваем события на кнопки function _bindHandlers() { $('#user-info-btn').on('click', _userInfo); $('#user-orders-btn').on('click', _userOrders); $('#user-last-order-btn').on('click', _userLastOrder); $('#user-total-summa-orders-btn').on('click', _userTotalSummaOrders); $('#user-new-btn').on('click', _userNew); $('#order-new-btn').on('click', _orderNew); $('#order-new-with-user-btn').on('click', _orderNewWithUser); $('#order-apply-bonus-btn').on('click', _orderApplyBonus); $('#user-apply-bonus-to-last-order-btn').on('click', _userApplyBonusToLastOrder); } // Инициализация приложения function init() { _bindHandlers(); } // Возвращаем наружу return { onCatchError: onCatchError, init: init } })(); // Запускаем приложение $(document).ready(app.init);
Здесь мы подготовили модуль app - главный модуль приложения. Переменная userId, глобальная для всего модуля, для примера берется рандомно. Дальше перечислены 9 функций, которые запускаются по нажатию на соответствующие кнопки. По комментариям Вы их легко отследите.
Функция onCatchError срабатывает, когда что-то в ajax-запросах пойдет не так. _bindHandlers навешивает все 9 событий кликов, init - базовая функция инициализации модуля. Блок return возвращает фасад модуля, то есть методы, доступные извне. onCatchError возвращается наружу, потому что будет использоваться в модулях user и order. Дальше идет $(document).ready(app.init) - это запускает приложение.
Вот такая получилась заготовка. Дальше разбираемся с самими промисами и примерами их использования.
Схема такая: непосредственно код, получающий данные и отправляющий их на сервер, будет располагаться в модулях user.js и order.js. То есть там основная реализация, ядро приложения. А в main.js в соответствующих местах, где сейчас стоит //... будет написано по несколько строк кода, использующего возможности модулей user и order.
Не волнуйтесь, если не до конца вкурили, сейчас все станет ясно.
Получаем информацию о пользователе
Первая задача - отправить ajax-запрос на сервер, получить инфу и что-то с ней сделать. Казалось бы чего здесь умничать? Мы тыщу раз так писали
$.ajax({ url: 'php/user/info.php', data: { userId: userId }, method: 'get', dataType: 'json', success: function(response) { // Что-то сделать с response }, error: function(response) { // Что-то сделать при ошибке }, });
Но не зря же мы захотели разобраться с промисами, да еще и запилили целый проект под это, хоть и небольшой. К тому же $.ajax уже возвращает промис и будет грешно этим не воспользоваться. Поэтому чуть выдохнем и напишем несколько другой код.
Файл /js/user.js
// Модуль Пользователь 'use strict'; var user = (function() { // Общая информация о пользователе // Возвращаем объект - email и бонус пользователя function getInfo(params) { return $.ajax({ url: 'php/user/info.php', data: { userId: params.userId }, method: 'get', dataType: 'json' }).then(function(data) { return { email: data.email, bonus: data.bonus }; }).fail(app.onCatchError); } // Возвращаем наружу return { info: getInfo } })();
Это каркас модуля user и первая функция, использующая промисы. Выглядит очень знакомо - тот же самый $.ajax, но для начала нет функции-колбэка. Вместо этого указываем, какие данные возвратить по результатам запроса. В блоке then ... return мы говорим, что ждем от промиса объект из двух полей: email и bonus. Как помним, именно они возвращаются с бекенда по запросу php/user/info.php.
getInfo(params) - в функцию передаем объект с параметрами. В нашем случае он один-единственный, userId, но Вы понимаете, что объект удобен тем, что в нем можно передать сколько угодно данных. Поэтому и в дальнейшем мы будем использовать объекты в качестве параметров. Впрочем, и в качестве ответа от сервера тоже.
.fail(app.onCatchError) говорит о том, что в случае каких-то неполадок с запросом нужно вызвать метод onCatchError модуля app. Неполадки могут быть разные: ошибка на стороне сервера, неправильный метод запроса или неверный url. Что делает этот метод onCatchError, пока не обращайте внимания, разберемся в конце статьи.
Еще заметьте, что метод user.info ничего не знает о том, как будут использоваться данные. Его задача - данные вернуть, а что с ними делать, пусть разбирается код, вызвавший user.info. И на мой взгляд, это круто, потому как видно четкое разделение логики. Модуль user знает, как взаимодействовать с сервером, но не знает, что хочет от него браузер. Этот модуль можно представить как прослойку между javascript-манипуляциями с dom-браузера и нашим бекендом. Точно так же, как и php-код является такой же прослойкой между браузером-клиентом и базой данных.
Но мы увлеклись созерцанием модуля и getInfo. Давайте взглянем, как использовать его на практике - это в функции _userInfo модуля app. Мы заботливо написали заглушку для нее, давайте же заполним кодом
// Получаем информацию о пользователе function _userInfo() { user.info({userId: userId}).done(function(userInfo) { console.log('userInfo: ', userInfo); }); }
А это все, что вызывается при клике на кнопку! Мы дергаем метод info модуля user. Не getInfo - это внутренняя функция. Именно info, так как мы ее указали в user в блоке return {...}
user.info({userId: userId}) возвращает нам промис. Дальше по цепочке вызываем done с функцией-колбэком, в которую передаем результат промиса. Напомню, это объект вида {email: 'webdevkin@gmail.com', bonus: 12345}. Ничего особенного делать с этим результатом не будем, просто выведем в консоли. Но никто не мешаем Вам выполнить с этими данными какие-то манипуляции. Возможно даже выделить для этого отдельную функцию, в которую передадите объект userInfo.
Давайте посмотрим еще раз на получившийся код и сравним его с привычным $.ajax запросом и выполнением всех нужных манипуляций в success-e. Вроде бы код тот же самый, разве что его стало немного больше. Но что-то все же изменилось. Код стал чище. Он разделился на 2 логические части: получение данных и их обработка. Меньше вложенности. Функции короче.
Если Вы еще не верите в практическую пользу такого подхода, то ниже я смогу убедить Вас в этом. Но сначала давайте напишем код для получения списка заказов пользователя - под копирку с предыдущим примером.
Получаем список заказов пользователя
Добавим в модуль user такой код
// Список заказов пользователя // Возвращаем массив заказов формата {id заказа, сумма заказа} function getOrders(params) { return $.ajax({ url: 'php/user/orders.php', data: { userId: params.userId }, method: 'get', dataType: 'json' }).then(function(data) { return data.orders; }).fail(app.onCatchError); } return { info: getInfo, orders: getOrders, }
А в main.js заполним кодом еще одну функцию
// Получаем список заказов пользователя function _userOrders() { user.orders({userId: userId}).done(function(userOrders) { console.log('userOrders: ', userOrders); }); }
Здесь ничего нового, отправили запрос, получили список заказов, вывели в консоль. Но вот уже в следующем абзаце будет намного интереснее.
Получаем последний заказ пользователя
Допустим, нам нужна информация не о всех заказах, а об одном, последнем. Бэкендское апи позволяет нам получить только все заказы целиком, в массиве. Проблем нет, мы легко вытащим отдельный объект заказа на клиенте, но как это организовать?
Конечно, нам помог бы тот же $.ajax, но сколько бы кода пришлось нам накопипастить при этом? Мы же не знаем, как часто придется вытаскивать последний заказ и в какие функции отправлять полученные данные. Конечно, можно упорядочить любой код, написать функции-обертки для ajax-запросов, и пробрасывать в $.ajax.success нужные функции-обработчики результатов... Но согласитесь, это не самый удобный способ, хотя и привычный
Поэтому мы в свете осваивания промисов сделаем такой финт ушами и напишем еще одну функцию в модуле user
// Последний заказ пользователя // Возвращаем последний заказ (объект) function getLastOrder(params) { return getOrders(params).then(function(orders) { return orders.slice(-1)[0]; }); } return { info: getInfo, orders: getOrders, lastOrder: getLastOrder, }
Посмотрите, что мы сделали. Мы взяли уже готовый метод, возвращающий другой промис, получащий список заказов. В orders в .then(function(orders)) попадает именно массив заказов, полученный с бекенда. Дальше лишь осталось вернуть последний элемент массива через return orders.slice(-1)[0];
Преимущество такой цепочки видно уже здесь. Функция getLastOrder даже не знает, каким образом вытаскиваются все заказы. Здесь не видно ajax-запроса, этим занимается другой метод. Мы только пользуемся готовым результатом.
А использование этого в main.js при клике на кнопку такое же простое, как и в предыдущих примерах
// Последний заказ пользователя function _userLastOrder() { user.lastOrder({userId: userId}).done(function(lastOrder) { console.log('userLastOrder: ', lastOrder); }); }
Давайте напишем еще одну функцию для закрепления.
Получение суммы всех заказов пользователя
В user.js
// Общая сумма заказов function getTotalSummaOrders(params) { return getOrders(params).then(function(orders) { return orders.reduce(function(total, currOrder) { return total + currOrder.summa; }, 0); }); } return { info: getInfo, orders: getOrders, lastOrder: getLastOrder, totalSummaOrders: getTotalSummaOrders }
В user.js
// Общая сумма всех заказов function _userTotalSummaOrders() { user.totalSummaOrders({userId: userId}).done(function(totalSumma) { console.log('userTotalSummaOrders: ', totalSumma); }); }
Мы использовали тот же приемчик. Взяли готовый метод getOrders(params) и высчитали сумму заказов через reduce. И опять мы видим вместо десятка строк кода с колбеками 2 коротких и внятных функции. К тому же user.totalSummaOrders() можно и дальше использовать где угодно, не заботясь о том, как будут использоваться возвращаемые ей данные.
Добавление нового пользователя
В user.js
// Добавление пользователя // Возвращаем id созданного пользователя function newUser(params) { return $.ajax({ url: 'php/user/new.php', data: { // какие-то данные о новом пользователе email: params.email, name: params.name }, method: 'post', dataType: 'json' }).then(function(data) { return data.userId; }).fail(app.onCatchError); } return { info: getInfo, orders: getOrders, lastOrder: getLastOrder, totalSummaOrders: getTotalSummaOrders, newUser: newUser }
В main.js
// Добавление пользователя function _userNew() { var data = { email: 'webdevkin@gmail.com', name: 'Webdevkin' }; user.newUser(data).done(function(userId) { console.log('userNew: ', userId); }); }
Выглядит чуть длиннее, но только из-за того, что мы передаем на сервер объект из двух полей. В остальном код такой же простой, как и при получении информации. Не забываем, что здесь уже используется method: 'post'
А теперь еще интереснее, начнем комбинировать ajax-запросы. Создадим новый модуль order.js.
Добавление заказа
order.js
// Модуль Заказ 'use strict'; var order = (function() { // Добавление заказа // Возвращаем id созданного заказа function _createOrder(params) { return $.ajax({ url: 'php/order/new.php', data: { // какие-то данные о новом заказе userId: params.userId, summa: params.summa }, method: 'post', dataType: 'json' }).then(function(data) { return data.orderId; }).fail(app.onCatchError); } // Добавление заказа // Возвращаем id созданного заказа function newOrder(params) { if (params.userId) { // userId известен, сразу добавляем заказ return _createOrder(params); } else { // Нужно создать сначала пользователя return user.newUser(params).then(function(userId) { return _createOrder({userId: userId, summa: params.summa}); }); } } // Возвращаем наружу return { newOrder: newOrder } })();
Здесь сразу 2 функции. _createOrder - это непосредственно добавление заказа. Работает аналогично с добавлением пользователя. Напрямую извне вызывать этот метод мы не будем - вся работа пойдет через newOrder.
Чем же таким особенным она занимается? Здесь нужно небольшое отступление...
Как работает оформление заказа в интернет-магазинах?
Часто оформление заказа проходит по двум схемам: для зарегистрированных пользователей и гостей.
Для зарегистрированных все понятно: берем userId (как правило, он хранится на бекенде в сессии),
передаем с клиента данные о заказе, добавляем заказ в базу. Лепота.
Но если на сайт зашел "гость", то ему мы тоже должны обеспечить оформление заказа.
Но завести на него пользователя все-таки нужно, заказ нужно тупо к кому-то привязывать.
Вы можете заставить человека отдельно зарегистрироваться на сайте, вернуться к оформлению,
и уже как полноправному пользователю отправить-таки данные о заказе на сервер.
А можно поступить гуманно: собрать минимум данных о пользователе (имя + email), данные о заказе и
одним махом и зарегистрировать пользователя, и добавить ему заказ.
Нет лишних телодвижений - больше радости покупателю.
Короче, получается, что функция newOrder умеет выполнять не только добавление заказа, но и предварительно добавить пользователя. Эта необходимость проверяется наличием параметра userId в params. Если его нет, то мы сначала добавляем пользователя, получаем его userId и уже с ним запускаем добавление заказа _createOrder
Если же params.userId известен, то _createOrder запускается напрямую. Достаточно сложные манипуляции реализуются теми же 5-6ю строками кода. С каждым разом мы берем на себя все более сложные задачи, но не наблюдаем существенного усложнения кода. Набор простейших методов, возвращающих промисы, позволяет нам как угодно комбинировать запросы и собирать сложный функционал как конструктор Лего.
Проверим, как работает добавление заказа в app.js - напишем обработчики еще для двух кнопок в main.js.
// Добавление заказа function _orderNew() { var data = { userId: userId, summa: 7000 }; order.newOrder(data).done(function(orderId) { console.log('orderCreate: ', orderId); }); } // Добавление заказа с созданием пользователя function _orderNewWithUser() { var data = { email: 'webdevkin@gmail.com', name: 'Webdevkin', summa: 10000 }; order.newOrder(data).then(function(orderId) { console.log('orderNewWithUser: ', orderId); }); }
Далее. Перед заключительным рывком из трех последовательных ajax-запросов напишем одну вспомогательную функцию order.applyBonus
Применяем бонус к заказу
order.js
// Применение бонуса к заказу // Возвращаем true в случае успеха function applyBonus(params) { return $.ajax({ url: 'php/order/applyBonus.php', data: { orderId: params.orderId, bonus: params.bonus }, method: 'post', dataType: 'json' }).then(function() { return true; }).fail(app.onCatchError); } // Возвращаем наружу return { newOrder: newOrder, applyBonus: applyBonus }
main.js
// Применение бонуса к заказу function _orderApplyBonus() { order.applyBonus({orderId: 5, bonus: 200}).then(function(result) { console.log('orderApplyBonus: ', result); }); }
Здесь все знакомо. Функция нужна нам как промежуточная, чтобы продемонстрировать, как выполнить 3 ajax-запроса. Нужно лишь придумать случай надобности 3 запросов подряд.
Да легко! Пофантазируем. На день рождения фирмы руководство решило начислить бонус всем своим клиентам и списать эту сумму с последнего заказа пользователя. Как бонус начислился, не наше дело, нам нужно получить сумму бонуса и id последнего заказа. Это 2 get-запроса, которые можно выполнить параллельно. Дождавшись этих данных, мы отправим еще один (третий) запрос на сервер, уже post, который и применит указанный бонус к нужному заказу.
Пример, конечно, притянут за уши, но ради искусства сойдет. Пишем код.
Применение бонуса к последнему заказу пользователя
user.js
// Применение бонуса к последнему заказу // Возвращаем true в случае успеха function applyBonusToLastOrder(params) { var infoPromise = this.info(params), lastOrderPromise = this.lastOrder(params); return $.when(infoPromise, lastOrderPromise).then(function(userData, lastOrderInfo) { return order.applyBonus({ orderId: lastOrderInfo.orderId, bonus: userData.bonus }); }); } return { info: getInfo, orders: getOrders, lastOrder: getLastOrder, totalSummaOrders: getTotalSummaOrders, newUser: newUser, applyBonusToLastOrder: applyBonusToLastOrder }
Здесь мы использовали новую конструкцию $.when. Смысл в том, что мы передаем ей несколько промисов, ждем их результатов, а в функцию then передаем колбек - что хотим с этими результатами делать. В нашем случае мы из первого вытаскиваем бонус пользователя, а из второго - id последнего заказа. И отдаем эти данные в третий метод. И все.
Это был последний и самый замороченный пример использования промисов в нашем тестовом проекте. Подумайте, сколько кода мы написали бы для такой последовательности раньше, не зная, как пользоваться промисами.
Проверяем работу в main.js так
// Применение бонуса к последнему заказу пользователя function _userApplyBonusToLastOrder() { user.applyBonusToLastOrder({userId: userId}).then(function(result) { console.log('userApplyBonusToLastOrder: ', result); }); }
Обработка ошибок
Наш мир не идеален, и на любом этапе запрос может прерваться. Нам нужно как-то обработать такие случаи и вспомнить про метод app.onCatchError. Давайте посмотрим на его реализацию
// Ловим ошибки в промисах function onCatchError(response) { var json = response.responseJSON, message = (json && json.error) ? json.error : response.statusText; console.log('ERROR: ', message); }
Что здесь происходит? В параметре response мы найдем информацию о настигнувшей нас ошибке. Сначала мы вытаскиваем содержимое response в объект json. А дальше проверяем его на наличие непонятного поля error.
Если мы вернемся в раздел с php-кодом, то увидим, что ничего загадочного в этом поле нет. Мы сами отдаем его с бекенда в случае несоответствия метода (http code 405). Когда, например, мы пытаемся создать пользователя методом GET. Я написал этот php-шный код исключительно чтобы показать, что мы сами можем формировать адекватные причины ошибок и сообщать о них пользователям. Точно так же можно выбрасывать такой ответ, если возникнет ошибка при валидации входных параметров или в процессе записи в базу данных.
Таким образом, в json.error мы найдем описание ошибки, формируемой на бекенде. Но это сработает в тех случаях, когда запрос до сервера дошел. Если же мы ошиблись, например, урлом или же сервер просто не отвечает, то в этом случае мы анализируем штатный ответ объекта xhr response.statusText.
Что делать с этой информацией об ошибке, решать Вам. Часто показывается какое-то ненавязчивое сообщение вроде "что-то пошло не так". Мы для примера напишем console.log('ERROR: ', message) и на этом закончим статью.
Итоги, демо и исходники
Надеюсь, я убедил Вас, что к промисам стоит присмотреться не только ради лишней строчки в резюме. Конечно, в статье мы и близко не рассмотрели все их возможности. По сути, мы затронули только частный случай - промисы, возвращаемые стандартным методом $.ajax, и мельком - $.when. Но зато убедились, что с этим можно работать и с их помощью писать простой и легко расширяемый код для довольно разнообразных и сложных задач.
Здесь можно скачать исходники рассмотренного кода. А тут посмотреть, как это добро работает. Просто нажимая на кнопки, Вы ничего не увидите. Открывайте консоль, вкладку Network и смотрите за порядком выполнения запросов, передаваемыми данными и возвращаемыми результатами. Убедитесь, что в нескольких последовательных запросах все параметры прокидываются правильно.
И конечно, делитесь собственными мыслями и соображениями в комментариях.
Истории из жизни айти и обсуждение кода.