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 и смотрите за порядком выполнения запросов, передаваемыми данными и возвращаемыми результатами. Убедитесь, что в нескольких последовательных запросах все параметры прокидываются правильно.
И конечно, делитесь собственными мыслями и соображениями в комментариях.
Истории из жизни айти и обсуждение кода.