jQuery Promise. Как заставить промисы работать на себя

январь 26 , 2017
Метки:
Демо Исходники

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

И конечно, делитесь собственными мыслями и соображениями в комментариях.

Демо Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!