Поэтапная асинхронная загрузка данных в jQuery (ajax)

Поэтапная асинхронная загрузка данных в jQuery (ajax)

Иногда возникает необходимость в поэтапной асинхронной загрузке данных. И это может быть все, что угодно. Начиная от загрузки зависимых справочников (например, страна -> города), заканчивая объединением и обработкой информации с различных источников (например, вы используете несколько разных сервисов, и каждый из них тратит разное время на формирование данных). 

А теперь чуть попроще.

 

Проблематика поэтапной асинхронной загрузки данных в jQuery (ajax)

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

// Первый запрос
$.ajax(url, {
...
success: function (result) {
    // Какой то код
    ...
    // Вызов следующего обработчика
    nextLoader();
}
});

А теперь, допустим, перед вами стоит чуть более сложная задача. Например, вам необходимо запустить функцию только после того, как выполнятся несколько асинхронных запросов (например, получение пузомерок для сайта). Конечно, их так же можно связать в прямую цепочку, как это было показано выше. Но, при этом начинает теряться сама суть асинхронности. И так же может получиться, что вы строите зависимость между совершенно не связанными блоками. Согласитесь, что показатель тИЦ и данные по whois не совсем зависят друг от друга.

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

Но, что вы будете делать, если ваша задаче окажется еще сложнее? Когда вам необходимо все операции и загрузки данных распределить по этапам. Например, вы составляете формочку диалога. На первом этапе будет идти получение данных для всех полей (выпадающие списки, группы чекбоксов и т.д.). На втором этапе будет происходить определение обработчиков для созданных полей. А на третьем этапе, уже после полной загрузки "всё и вся", открывать доступ к различным кнопкам и другим элементам интерфейса (чтобы шаловливые ручки не создали вам кучи проблем). Чисто визуально это может выглядеть так:

Поэтапная асинхронная загрузка данных в jQuery (ajax)

Согласитесь, что организовать вызов всех функций в одну цепочку - ни только не простое занятие, но еще и не очень приятное. Кроме этого еще представьте, что вам надо добавить несколько полей и различных обработчиков. Что будет? У вас начнет голова закипать от поиска нужных мест "куда вставить". Тут уже нужен какой-то механизм, который позволит вам выстроить все запуски поэтапно.

 

Пишем скрипт для организации поэтапной асинхронной загрузки в jQuery

Первым делом необходимо определиться с вполне логичным вопросом "почему не использовать готовые библиотеки?". Как такового, ответа на этот вопрос не существует. Все очень сильно зависит от задачи. Так как слово "библиотека" обычно подразумевает:

  • отсутствие управляемости кода (вы не можете поменять внутреннюю логику библиотеки; а если вы это сделаете, то любое изменение может сильно "аукнуться" в будущем)
  • необходимость изучения ее базовый свойств и принципов (обычно, это сводится к прочтению всей возможной документации и куче различных тестов)
  • ограничения на версии jQuery (если используются сложные ядровые механизмы, то возможно привязка к версии библиотек jQuery)
  • возможные проблемы совместимости с другими скриптами (далеко не факт, что библиотека будет корректно себя вести с другими библиотеками)
  • чрезмерное богатство функциональности, и как следствие наличие "дополнительных ограничений"
  • и т.д.

Примечание: Не стоит входить в заблуждение, ведь у библиотек есть огромное количество плюсов. Если вспомнить, то и jQuery - это то же библиотека. В статье акцент фокусируется на рисках и дополнительных затратах времени, которые от вас могут потребоваться.

Именно, поэтому необходимо сначала определиться с вашими задачами (количество, частота возникновения), со временем (сколько вы готовы потратить на решение задачи), чем вы готовы пожертвовать (важна ли для вас возможность быстро что-то подправить; важен ли размер библиотек; насколько ограничения вас устраивают), предпочтениями (например, вы принципиально не используете библиотеки). И уже только после того, как вы ответите себе на эти вопросы, пытаться ответить на вопрос "почему стоит или не стоит использовать готовые библиотеки?".

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

Если же вы полны энтузиазма и решимости, то преступим к дальнейшим действиям.

 

Составляем требования к скрипту

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

  • Подключение и настройка скрипта должны происходить автоматически (вам необходимо подключить лишь файл js)
  • Повторное подключение файла скрипта не должно вызывать сбой
  • Использовать только стандартные механизмы jQuery
  • Интерфейс должен быть простым и понятным 
  • Вы можете составлять несколько независимых цепочек этапов
  • У вас всегда должна быть возможность быстро подкорректировать действия скрипта
  • Скрипт не должен накладывать сложные ограничения на ваш код (максимально - необходимость в добавлении функции оповещения о своем выполнении)

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

 

Составляем тестовый проект

Структура проекта будет выглядеть следующим образом:

  • css - каталог со стилями
    • template.css - стили тестового проекта
  • images - каталог с картинками (для визуального эффекта)
    • ajax-loader.gif - картинка загрузки
  • js - каталог со скриптами
    • jquery-ajax-stage.js - скрипт для поэтапной загрузки
  • data.json - тестовые данные 
  • index.html - начальная страница

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

 

Файл с тестовыми данными - data.json

Этот файл нужен только для того, чтобы можно было убедиться, что загрузка данных через ajax никак не повлияет. Вы можете наполнить его любым json. Например, так:

{ data: 'Много данных' }

Примечание: Старайтесь делать так, чтобы тестовые примеры были максимально схожи с реальными. Пусть даже вызовы функций будут бессмысленными.

 

Файл стилей - template.css

Этот файл нужен только затем, чтобы вынести все стили отдельно. Так как к самой разработке скрипта стили не имеют никакого отношения. Да, и просто это считается хорошей практикой. Сами стили:

.left-table {
    float: left;
    padding-right: 10px;
}
.right-table {
    float: left;
    padding-right: 10px;
}
table.table-loader {
    border-spacing: 0px;
    border-collapse: collapse;
    width: 300px;
}
table.table-loader tr td {
    padding: 5px;
    border: 1px solid #ccc;
}

 

Файл скрипта для поэтапной асинхронной загрузки  - jquery-ajax-stage.js

Сама реализация поэтапной асинхронной загрузки.

(function (parentObject) {
    // Защита от повторного определения
    if(parentObject.eventManager)
        return;
        
    // Определяем объект
    parentObject.eventManager = {
        // Добавляем этап
        addStage: function (el, stageName) {
            var elCount = $(el).length;
            // Проверяем корректность параметров
            if(elCount > 0 && !!stageName) {
                // Такой этап уже есть
                if (($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName])
                    return;
                
                // Определяем 'nfg
                $(el).get(0).eventStages[stageName] = { 
                    waiterCount: 0, // Счетчик объектов находящихся в состоянии ожидания данных
                    onEvent: [] // в данном массиве будут хранится все функции для вызова
                };
            }
        },
        
        // Удаляем этап
        removeStage: function (el, stageName) {
            var elCount = $(el).length;
            // Проверяем корректность параметров
            if(elCount > 0 && !!stageName) {
                // Такой этап нашелся
                if (($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName]) {
                    delete ($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName];
                    ($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName] = null;
                }
            }
        },
        
        // Увеличиваем счетчик выполняемых функций для этапа
        addStageWaiter: function (el, stageName) {
            var elCount = $(el).length,
                stage = ($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName];
            // Проверяем корректность параметров
            if(elCount > 0 && !!stageName && stage) {
                // Увеличиваем счетчик загрузок
                stage.waiterCount++;
            }
        },
 
        // Уменьшаем счетчик выполняемых функций для этапа
        // Т.е. оповещаем, что функция выполнилась
        removeStageWaiter: function (el, stageName) {
            var elCount = $(el).length,
                stage = ($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName];
            // Проверяем корректность параметров
            if(elCount > 0 && !!stageName && stage) {
                // Уменьшаем счетчик загрузок
                stage.waiterCount--;
                // Проверяем состояние этапа
                this.checkStage(el, stageName);
            }
        },
        
        // Проверяем состояние этапа
        checkStage: function (el, stageName) {
            var elCount = $(el).length,
                stage = ($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName];
            // Проверяем корректность параметров
            if(elCount > 0 && !!stageName && stage) {
                if (stage.waiterCount <= 0) {
                    while (stage.onEvent.length > 0) {
                        // Очередь FIFO - первый вошел, первым вышел, как в магазине
                        stage.onEvent.shift()();
                    }
                }
            }
        },
        
        // Добавляем вызов функции, который запустится при выполнении всего этапа
        onLoadStage: function (el, stageName, funcHandler) {
            var elCount = $(el).length,
                stage = ($(el).get(0).eventStages = $(el).get(0).eventStages || {})[stageName];
            // Проверяем корректность параметров
            if(elCount > 0 && !!stageName && stage && typeof (funcHandler) === 'function') {
                // Добавляем обработчик
                stage.onEvent.push(funcHandler);
                // Проверяем состояние этапа
                this.checkStage(el, stageName);
            }
        }
    };
})(window);

 

Тестовая страница - index.html

Тестовая страница получилась достаточно большой (порядка 160 строк), поэтому будет приведена только часть кода. Полную версию файла вы всегда можете найти в zip-архиве. 

Подключаем все необходимые файлы:

<link href="https://code.jquery.com/ui/1.11.0/themes/ui-lightness/jquery-ui.css" rel="stylesheet" type="text/css"/>
<link href="/css/template.css" rel="stylesheet" type="text/css"/>
<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="/js/jquery-ajax-stage.js"></script>

Составляем пару тестовых блоков. В блок входит кнопка для запуска и таблица куда поэтапно будут записываться данные.

<div class="left-table">
    <button class="start-process">Начать загрузку</button>
    <table class="table-loader">
        <tr class="row-1">
            <td class="td-1"></td><td class="td-2"></td><td class="td-3"></td>
        </tr>
        <tr class="row-1-end">
            <td colspan="3"></td>
        </tr>
        <tr class="row-2">
            <td class="td-1"></td><td class="td-2"></td><td class="td-3"></td>
        </tr>
        <tr class="row-2-end">
            <td colspan="3"></td>
        </tr>
        <tr class="row-3">
            <td class="td-1"></td><td class="td-2"></td><td class="td-3"></td>
        </tr>
        <tr class="row-3-end">
            <td colspan="3"></td>
        </tr>
    </table>
</div>

Определим функцию, которая будет создавать тестовые функции для этапов:

function getFakeLoader(storage, currStage, startDate, selector) {
    return function () {
        setTimeout(function () {
            $.ajax('data.json?callback=?', {    
                contentType: "text/plain; charset=utf-8",
                dataType: 'jsonp',
                jsonpCallback: function (result) {
                    $(selector).html('Время от начала: ' + ((+new Date() - startDate)/1000.0).toFixed(2) + ' секунд'); // Пишем время загрузки
                    window.eventManager.removeStageWaiter(storage, currStage); // Уменьшаем счетчик
                }
            });
        }, Math.random() * (3000) + 1000 // Задежрка от 1 до 4 секунд);
    };
}

Создаем функцию, которая будет описывать логику для каждого тестового блока:

function formTestFor(selector)
{
    $(selector + ' .start-process').click(function () {
        var startDate =  new Date();
        // Для красоты добавим картинки
        setLoaderImgs(selector);
        // Определим этапы
        window.eventManager.addStage($(selector + ' .table-loader'), '1');
        window.eventManager.addStage($(selector + ' .table-loader'), '2');
        window.eventManager.addStage($(selector + ' .table-loader'), '3');
        
        // Заблокируем кнопку до конца этапа
        $(selector + ' .start-process').attr('disabled', 'disabled');
        
        // Наполним очередь ожидания по 3 загрузки на стейдж
        // По факту эти действия должны происходить в местах запуска функций этапов
        window.eventManager.addStageWaiter($(selector + ' .table-loader'), '1');
        ...
        window.eventManager.addStageWaiter($(selector + ' .table-loader'), '3');
        
        // Теперь создадим в обратном порядке (для наглядности) функции загрузки
        // По факту эти действия должны происходить в местах определения функций
        window.eventManager.onLoadStage($(selector + ' .table-loader'), '2', getFakeLoader($(selector + ' .table-loader'), '3', startDate, selector + ' .row-3 .td-1'));
        ...
        window.eventManager.onLoadStage($(selector + ' .table-loader'), '1', getFakeLoader($(selector + ' .table-loader'), '2', startDate, selector + ' .row-2 .td-3'));
        
        // Добавим обычный текстовый вывод готовых этапов
        window.eventManager.onLoadStage($(selector + ' .table-loader'), '1', function () {
            $(selector + ' .row-1-end td').html('Первый этап закочнился за ' + ((+new Date() - startDate)/1000.0).toFixed(2) + ' секунд. Начинается следующий этап.');
        });
        ...
        window.eventManager.onLoadStage($(selector + ' .table-loader'), '3', function () {
            $(selector + ' .row-3-end td').html('Третий этап закочнился за ' + ((+new Date() - startDate)/1000.0).toFixed(2) + ' секунд.  <br/>Загрузка окончена');
        });
 
        // После окончания третьего этапа разблокируем кнопку onLoadStage
        window.eventManager.onLoadStage($(selector + ' .table-loader'), '3', function () {
            // Разблокируем кнопку
            $(selector + ' .start-process').attr('disabled', null);
        });
        
        // Теперь запустим функции первого этапа
        getFakeLoader($(selector + ' .table-loader'), '1', startDate, selector + ' .row-1 .td-1')();
        getFakeLoader($(selector + ' .table-loader'), '1', startDate, selector + ' .row-1 .td-2')();
        getFakeLoader($(selector + ' .table-loader'), '1', startDate, selector + ' .row-1 .td-3')();
        
        // Наблюдаем...
    });
}

Теперь собираем все файлы в проект и переходим к первичному тестированию и демонстрации.

 

Смотрим результат

Открываем файл index.html в браузере. Должен отобразиться следующий интерфейс:

Поэтапная асинхронная загрузка данных в jQuery (ajax)

Как видно, нам доступно две кнопки для запуска параллельных процессов. После нажатия на них, должна начаться загрузка для соответствующих блоков. И выглядеть это будет примерно так:

Поэтапная асинхронная загрузка данных в jQuery (ajax)

После того, как оба процесса закончили свое выполнение, смотрим на время и убеждаемся, что этапы происходили в нужном порядке:

4.end

Закончив первичное тестирование, переходим к проверке требований:

  • Подключение и настройка скрипта должны происходить автоматически (вам необходимо подключить лишь файл js) - Есть
  • Повторное подключение файла скрипта не должно вызывать сбой - Есть 
  • Использовать только стандартные механизмы jQuery - Есть (использовался только функционал селекторов)
  • Интерфейс должен быть простым и понятным - Есть
  • Вы можете составлять несколько независимых цепочек этапов - Есть
  • У вас всегда должна быть возможность быстро подкорректировать действия скрипта - Есть (скрипт составлен достаточно просто; основную часть составляют проверки входных данных)
  • Скрипт не должен накладывать сложные ограничения на ваш код (максимально - необходимость в добавлении функции оповещения о своем выполнении) - Есть (внутри функций getFakeLoader вызывается только функция для оповещения о своем завершении)

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

Примечание: В тестовом примере не приводятся обработчики ошибок загрузки данных и т.п., потому что они зависят от задачи и от самих функций. А так же потому что это не относится к скрипту поэтапной загрузки. 

Ссылка для скачивания доступна тут:

ajax_stages.zip

☕ Понравился обзор? Поделитесь с друзьями!

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

Комментарий - это вежливое и наполненное смыслом сообщение (правила).



* Нажимая на кнопку "Отправить", Вы соглашаетесь с политикой конфиденциальности.
Присоединяйтесь
 

 

Программы (Freeware, OpenSource...)