пятница, 30 января 2015 г.

Пример jQuery Deferred при работе с Ajax сервисами

Если вам приходилось работать с web сервисами на javascript, возможно вы обращали внимание на то, что у них четкая структура ответа.

Например:

{
    "status": "401", 
    "message": "Authentication Required."
}

или 
{
    "status": "200",
    "message": null,
    "products": [
         { 
            "id": 1, 
            "title": "t-shirt",
            ...
         },
         ...
    ]
}

Интересные статьи по дизайну API можно почитать здесь, там же можно скачать брошюрку по дизайну REST сервисов.

В этой статье мы будем говорить об использовании jQuery $.Defered на примере структуры запросов, которые использовал я у себя в проектах.

Пример моей структуры:


{
    "success": true, /* Флаг об успешном выполнении операции. */ 
    "error": null, /* Текст или объект ошибки, который можно показывать пользователю. */
    "data": null /* Массив или объект. */
}


GET /employee/list

{
    "success": true,
    "error": null,
    "data": [
         {
             "id": 1,
             "name": "Eaton Cross",
             "email": "Duis.risus.odio@consectetuereuismod.org",
             "phone": "1-215-898-1399"
         },
         {
             "id": 2,
             "name": "Randall Johnson",
             "email": "vitae.diam.Proin@Sedcongue.ca",
             "phone": "1-918-519-4417"
         },
         ...
    ]
}


POST /employee/create

{
    "success": true,
    "error": "Email 'vitae.diam.Proin@Sedcongue.ca' already in use. Please provide another email.",
    "data": null
}


Пример использования:

$.getJSON('/employee/list')
    .success(function(response) {
        if (response.success == true) {            
            // 1. Парсим данные из response.data
            // 2. Показываем данные
        }
        else {
            // Показывает ошибку(и) из response.error
        }
    })
    .error(function(xhr, error, statusText){
         /* Показывает обший текст ошибки, т.к. сюда приходят ошибки сервера 
           (если запрос не обработался правильно) 
           и ошибка в следствии перезагрузки страницы до зовершения запроса. */
    })
    .always(function(){
         hideLoad() // Скрываем ajax крутилку
    });


Преимущество подобной структуры является однотипность, вы всегда знаете что у вас есть данные в одном поле, ошибка и сообщение в другом.

В этом примере есть недостатки.

  • В обработчике success у вас всегда код будет начинатся одинаково, с проверки на успешное выполнение. С одной стороны, вроде бы ничего, с другой, если код выполнился не по плану, значит он должен прийти в обработчик fail. Так же логичней? А в success только обработчик успешного выполнения кода на серевер. 
  • Другая проблема в том, что сейчас в error будет вызыватся если пользователь перегружает страницу до завершения выполнения запроса или если запрос выполнился с ошибкой на сервере или страница не найдена и тп.


Решим эти проблемы используя код с предыдущей статьи  немного усовершенствовав его.
Для начала напишем следующую функцию:

function handleResponse (ajaxRequest) {
    var isUserAbortedRequest = function (xhr) {
        return !xhr.getAllResponseHeaders();
    }

    return $.Deferred(function (def) {
        ajaxRequest
            .error(function(xhr, error, statusText) {
                def.reject({ aborted: isUserAbortedRequest(xhr), error: error });
            })
            .success(function (response) {
                if (response.success == true) {
                    def.resolve(response.data);
                }
                else {
                    def.reject({ aborted: false, error: response.error });
                }
            });
        });
};

Эта функция нам пригодится, чтоб "обвернуть" ajax запрос и добавить изменения для обработки повторяющегося кода, возникающего в событии success, описанного выше.

Каждый ajax запрос посылаемый с помощью jQuery возвращает объект jQuery promise к которому можно цеплять обработчики done\success, error\fail, complete\always и таким образом следить за процессом выполения асинхронной операции.

Promise - это шаблон проектирования, улучшающий органиазцию кода при работе с асинхронными операциями.
Шаблон используется также при работе с анимациями, например обработчики fadeIn, show и др подобные тоже возвращают promise. 

Очень удобная функция $.when - она позволяет вам получать уведомления о процессе выполнения группы асинхронных операций. Допустим вам надо показать какое-то сообщение пользователю только тогда, когда все 3 ajax запроса выполнятся.

Подбробней можно почитать на сайте jQuery  и здесь. Возможно я еще напишу о применени этого шаблона с примерами.

Теперь по коду.
Функция handleResponse принимает ajax запрос и возвращает обвертку в виде $.Deferred. Для клиент-кода, который будет использовать эту функцию ничего не изменится.

Наш перехватчик-обвертка смотрит, если запрос не выполнился на сервере, то в обработчике error мы проверяем почему он не выполнился, скорее всего из-за перезагрузки страницы или ошибки на сервере и говорим нашему promise объекту, чтоб он завершил наш ajax запрос, с ошибкой и аргументами aborted и error.

Если мы получили ответ от сервера, смотрим успешно ли он выполнился, если нет, говорим нашему объекту promise завершить наш ajax запрос с ошибкой и передаем текст ошибки, который можно будет показать пользователю.

Если все ОК, успешно завершаем наш promise объект с response.data.

Сейчас код можно использовать так:

handleResponse($.getJSON('/employee/list'))
    .done(function(response) {
         // Уже работаем с данными через response.employees
    })
    .fail(function(e) {
        // Если это наша ошибка, показываем. В другому случае ничего не делаем
        if (!e.aborted) {
            alert(e.error);
        }
    })
    .always(function(){
         hideLoad() // Скрываем ajax крутилку
    });

Success и error обработчики только для jQuery ajax запросов, это было сделано просто для логичных называний. Поскольку мы возвращаем обвертку через promise, то эти обрабочики недоступный, вместо них более общие done\fail.

Цель выполнена, мы избавились от дублирующего кода и теперь результат ajax запроса приходит в обработчики как должно быть, по назначению и без лишних проверок.

В таком виде код не очень удобно использовать, тем более есть еще POST запрос, который надо настраивать если вы общаетесь с сервером в JSON формате. Поэтому его надо отрефакторить.

Вот, что получилось в итоге - легковесный класс для ajax запросов

var Ajax = (function ($) {

    var _postJson = function (url, data) {
        return $.ajax({
            type: "POST",
            url: url,
            data: ko.toJSON(data),
            dataType: 'json',
            contentType: 'application/json'
        });
    };

    var _getJson = function (url, data) {
        return $.getJSON(url, data);
    };

    var _handleResponse = function (ajaxRequest) {
        var isUserAbortedRequest = function (xhr) {
            return !xhr.getAllResponseHeaders();
        }

        return $.Deferred(function (def) {            
            ajaxRequest
                .fail(function(xhr, error, statusText) {
                    def.reject({ aborted: isUserAbortedRequest(xhr), error: error });
                })
                .success(function (response) {
                    if (response.success == true) {
                        def.resolve(response.data);
                    }
                    else {
                        def.reject({ aborted: false, error: response.error });
                    }
                });
        });
    };

    /* exports */
    return {
        post: function (relativeUrl, data) {
            return _handleResponse(_postJson(relativeUrl, data));
        },
        get: function (relativeUrl, data) {
            return _handleResponse(_getJson(relativeUrl, data));
        }
    };

})(jQuery);


Использовать так:

Ajax.get('/employee/list')
    .done(function(response) { // Уже работаем с данными через response.employees })
    .fail(function(e) {        
        if (!e.aborted) {
            alert(e.error);
        }
    });

И для POST запросов

var parameters = {
    name: 'Jhon', 
    email: '...'
};

Ajax.post('/employee/create', parameters)
    .done(function(response) { 
        // Employee создался, возможно в response будет response.employee или id завист от вашего дизайна. 
    })
    .fail(function(e) {        
        if (!e.aborted) {
            /* Наш employee не создался, возможно валидация, показываем. 
               Возможно в e.error будет объект c валидационными ошибками. Это уже вам решать. */
            alert(e.error); 
        }
    });


Пока все

пятница, 23 января 2015 г.

Ajax запрос и ошибка при перезагрузке страницы

Всем привет,

Возможно вы сталкивались с ситуацией, когда вы делаете ajax запрос, он долго орабатывается на сервере и вы перегружаете страницу в надежде, что следующий раз он выполнится быстрей, но нет :) вы получаете ошибку.
Ниже пример такого запроса:
$.getJSON("/clients/list")
    .success(function(){ ... })
    .error(function(xhr, error, statusText){
 // ошибка выпадает здесь
    })

Ошибка в указанном месте может возникать не только из-за того, что пользователь не дождался выполнения запроса до конца и перешел на другую страницу или перегрузил страницу во время выполенения, но и из-за ошибок на сервер, например 503 и тп.

Но нас интересует именно случай, когда обработчик error срабатывает когда пользователь перегружает или уходит со страницы вовремя выполнения запроса, в этом случае браузер прерывает запрос.

Как правило, в функции error разработчики показывают диалог с ошибкой на экран или информируют пользователя другими способами об ошибке.

Когда у вас ajax запрос выполняется с ошибкой, у вас в обработчике нет никаких флагов и полей, которые говорят почему запрос не полнился. В документации к jquery http://api.jquery.com/jQuery.ajax/ пишут, что в поле error (в моем примере) будет статус запроса. На практике такого не было. Напишите в коментариях, если у вас статусы работают.

Теперь решение.
Если запрос выполнился, но с ошибкой (на сервере), вы получите полноценный HTTP ответ с хедерами и телом ответа, не важно каким. Но, если запрос был прерван - вы не получите HTTP ответ, следовательно вы не получите ни хедеры, ни тело ответа. Все :)

Для проверки того, что запрос выполнился с ошибкой из-за перезагрузки страницы или ухода с нее, достаточно проверить хедеры ответа.

Код с решением:
var isUserAbortedRequest = function (xhr) {
    return !xhr.getAllResponseHeaders();
}

$.getJSON("/clients/list")
    .success(function(){ ... })
    .error(function(xhr, error, statusText){
 if (!isUserAbortedRequest(xhr)) {
     // показаем ошибку
        } else {
     // ничего не показываем, перезагрузка или переход на другую страницу
        } 
    })