jQuery и Ajax

198

Ajax — это сокращение от "Asynchronous JavaScript and XML" (асинхронный JavaScript и XML), но в наши дни этот термин употребляется как самостоятельный, без приписывания ему какого-либо смысла в результате расшифровки названия.

Ajax позволяет отправлять на сервер асинхронные запросы. В двух словах это означает, что обмен данными между браузером и сервером выполняется в фоновом режиме, и поэтому никоим образом не мешает взаимодействию пользователя с содержимым HTML-документа. Чаще всего Ajax используется для отправки данных формы на сервер. Преимуществом такого подхода является то, что отображение ответной информации, получаемой от сервера, не требует полной перезагрузки веб-страницы, и данные документа могут беспрепятственно отображаться с помощью стандартных функций jQuery.

Средства поддержки Ajax, которые используются в этой статье, встроены в основную библиотеку jQuery. Наличие этой поддержки в jQuery значительно упрощает использование программного интерфейса Ajax, предусмотренного в браузерах.

Обычно средства Ajax ассоциируются с отправкой данных формы, однако их реальная сфера применения гораздо шире. Изучение методов Ajax начнем с выполнения простейших задач. Рассмотрим способы получения данных с веб-сервера без привлечения форм.

В библиотеке jQuery определен ряд так называемых прямых методов, которые в действительности представляют собой оболочки для вызовов функций ядра Ajax и позволяют быстро и просто решать типичные задачи Ajax.

Кратко об Ajax

Если вы новичок в Ajax, позвольте вкратце рассказать вам о том, что такое асинхронные запросы. Это важно знать, поскольку асинхронные запросы занимают в Ajax центральное место, а буква А в Ajax происходит от слова asynchronous.

Как программист большую часть времени вы тратите на написание синхронного кода. Вы определяете блок кода, решающий некоторую задачу, и остальная часть программы ждет, пока этот блок не выполнится. Задача завершается выполнением последнего оператора блока. На то время, пока выполняется блок, браузер лишает пользователя возможности взаимодействовать с содержимым веб-страницы.

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

В случае Ajax вы приказываете браузеру связаться с веб-сервером и сообщить вам, когда запрос будет выполнен. Управление этой связью осуществляется с помощью функций обратного вызова (callback functions). Вы предоставляете браузеру одну или несколько функций, которые должны быть вызваны сразу же по завершении выполнения задачи. Должна быть предусмотрена функция, которая обработает успешный запрос, а кроме того, могут существовать функции, выполняющиеся в случае других исходов, например при возникновении ошибок.

Преимущество асинхронных запросов состоит в том, что они позволяют создавать функционально насыщенные HTML-документы, которые могут непрерывно обновляться на основании полученных от сервера ответов, не прерывая взаимодействия пользователя с приложением и не заставляя его дожидаться окончания полной загрузки документа.

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

Выполнение GET-запросов Ajax

Прежде всего, Ajax используется для того, чтобы выполнить HTTP-запрос GET с целью загрузки HTML-фрагмента, который можно добавить в документ. Образец документа, с которым мы будем работать, приведен ниже:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Библиотека jQuery</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>	
$(function() {
	
    // Сюда вставляется тестируемый код
		
});
</script>
</head>
<body>
    <h1>Цветочный магазин</h1>
    <form method="post">
        <div id="oblock">        
            <div class="dtable">
                <div id="row1" class="drow"></div>
                <div id="row2" class="drow"></div>            
            </div>
        </div>
        <div id="buttonDiv"><button type="submit">Заказать</button></div>                    
    </form>
</body>
<style>
h1 {
    min-width: 70px; border: thick double black; margin-left: auto;
    margin-right: auto; text-align: center; font-size: x-large; padding: .5em;
    color: darkgreen; background-image: url("http://professorweb.ru/downloads/jquery/border.png");
    background-size: contain; margin-top: 0;
}
.dtable {display: table;}
.drow {display: table-row;}       
.dcell {display: table-cell; padding: 10px;}
.dcell > * {vertical-align: middle}
input {width: 2em; text-align: right; border: thin solid black; padding: 2px;}
label {width: 6em;  padding-left: .5em; display: inline-block;}
#buttonDiv {text-align: center;}
button {padding: 12px;}
#oblock {display: block; margin-left: auto; margin-right: auto; min-width: 700px; }
</style>
</html>

Этот код (файл test.html) аналогичен тому, который использовался в предыдущих примерах, но в нем отсутствуют элементы, описывающие отдельные виды цветочной продукции, а также элементы данных и шаблоны, с помощью которых они генерируются. Вместо этого создан отдельный файл под названием flowers.html:

<div>
    <img src="http://professorweb.ru/downloads/jquery/astor.png"/>
    <label for="astor">Астра:</label>
    <input name="astor" value="0" required>
</div>
<div>
    <img src="http://professorweb.ru/downloads/jquery/daffodil.png"/>
    <label for="daffodil">Нарцисс:</label>
    <input name="daffodil" value="0" required >
</div>
<div>
    <img src="http://professorweb.ru/downloads/jquery/rose.png"/>
    <label for="rose">Роза:</label>
    <input name="rose" value="0" required>
</div>
<div>
    <img src="http://professorweb.ru/downloads/jquery/peony.png"/>
    <label for="peony">Пион:</label>
    <input name="peony" value="0" required>
</div>
<div>
    <img src="http://professorweb.ru/downloads/jquery/primula.png"/>
    <label for="primula">Примула:</label>
    <input name="primula" value="0" required>
</div>            
<div>
    <img src="http://professorweb.ru/downloads/jquery/snowdrop.png"/>
    <label for="snowdrop">Подснежник:</label>
    <input name="snowdrop" value="0" required>
</div>  

Это те же элементы, которые использовались в предыдущих статьях, но они не распределены по рядам и из элементов div удален атрибут class. Эти изменения внесены исключительно для того, чтобы продемонстрировать, как осуществляется обработка элементов, загружаемых в документ. Никаких технических предпосылок, которые заставляли бы поступать именно так, нет. Обратите внимание на то, что это не полноценный HTML-документ, а всего лишь фрагмент.

Для тестирования всех приведенных ниже и в следующей статье примеров, вы должны сохранить оба этих файла на локальном сервере. Если вы еще не установили локальный сервер, рекомендую WAMP сервер EasyPHP.

Для включения этого фрагмента в основной HTML-документ мы можем воспользоваться поддержкой Ajax, предоставляемой jQuery. Возможно, вас удивляет использование такого подхода, однако мы используем его, поскольку он имитирует распространенную ситуацию, когда сложный документ или веб-приложение получают путем "сшивки" отдельных фрагментов содержимого, создаваемого различными системами.

Пример того, как это можно сделать, приведен ниже:

$(function() {
	
    $.get('flowers.html', function(data) {
		 $(data).filter('div').addClass('dcell')
		        .slice(0, 3).appendTo('#row1').end().end()
		        .slice(3).appendTo('#row2')
	});
		
});

Здесь используется метод get(), которому передаются два аргумента. Первый из них — это URL-адрес, указывающий на документ, который мы хотим загрузить. В данном случае используется адрес flowers.html, который будет интерпретироваться как URL, заданный относительно URL-адреса, использующегося для загрузки основного документа.

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

Когда вы будете загружать документ, содержащий этот сценарий, файл flowers.html загрузится с сервера, браузер выполнит его синтаксический анализ, и полученные элементы добавятся в документ. Конечный результат представлен на рисунке:

Результат использования Ajax

В этом сценарии метод get() используется для загрузки HTML-страницы, однако точно так же с сервера могут быть загружены данные любой природы.

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

Обработка ответных данных сервера

Функция, выполняющаяся в случае успешного завершения запроса, принимает в качестве аргумента данные, отправленные сервером в ответ на запрос. В этом примере мы получаем содержимое файла flowers.html, представляющее собой HTML-фрагмент. Чтобы превратить этот фрагмент в объект, с которым можно работать средствами jQuery, мы передаем его функции $(), которая выполнит синтаксический анализ фрагмента и сгенерирует дерево объектов HTMLElement, как показано в примере ниже:

...
   
function(data) {
		 $(data).filter('div').addClass('dcell')
		        .slice(0, 3).appendTo('#row1').end().end()
		        .slice(3).appendTo('#row2')
});

Как уже отмечалось, из элементов div были намеренно удалены атрибуты class. Теперь, как видите, мы восстанавливаем их с помощью стандартного метода addClass(). Передав данные функции $(), мы получаем от нее объект jQuery, с которым далее можем работать как с любым другим объектом. Мы добавляем элементы в документ с помощью методов slice() и appendTo().

Обратите внимание на то, что для выбора элементов div, сгенерированных на основе полученных от сервера данных, используется метод filter(). Дело в том, что в процессе синтаксического анализа символы перевода строки, введенные между элементами div в файле flowers.html для структурирования данных, jQuery воспринимает как текстовое содержимое и вставляет вместо них текстовые элементы. Чтобы этого избежать, необходимо либо проследить за тем, чтобы эти символы отсутствовали в запрашиваемом документе, либо удалить их, используя метод filter().

Повышение наглядности процесса загрузки

В нашем сценарии инструкции, запускающие Ajax-запрос, начинают выполняться, когда срабатывает событие ready. Это мешает увидеть различия между использованием Ajax и встроенных данных. Для повышения наглядности процесса загрузки добавим в документ кнопку и сделаем так, чтобы Ajax-запрос выполнялся лишь после того, как на этой кнопке будет выполнен щелчок. Вносимые изменения отражены в примере ниже:

$(function() {
	
	$('<button>Ajax</button>').appendTo('#buttonDiv').click(function(e) {
		$.get('flowers.html', function(data) {
			 $(data).filter('div').addClass('dcell')
		  	      .slice(0, 3).appendTo('#row1').end().end()
		  	      .slice(3).appendTo('#row2')
		});
		
		e.preventDefault();
	});    
		
});

Теперь документ flowers.html не будет загружаться до тех пор, пока на кнопке "Ajax" не будет выполнен щелчок, причем каждый последующий щелчок также будет приводить к добавлению в документ дополнительных элементов, как показано на рисунке:

Использование Ajax после щелчка на кнопке

Обратите внимание на вызов метода preventDefault() для объекта Event, который передается в обработчик событий. Это делается для того, чтобы отменить выполнение браузером действий, предусмотренных по умолчанию. Поскольку элемент button содержится внутри элемента form, действием по умолчанию является отправка формы на сервер.

Получение других типов данных

Возможности метода get() не ограничиваются работой только с HTML. Он позволяет получать с сервера данные любой природы. Особый интерес для нас представляет формат JSON, предоставляющий удобные возможности для обработки данных средствами jQuery.

В свое время, когда технология Ajax еще только начинала широко внедряться, предпочтительным форматом данных считался XML, и даже буква X в аббревиатуре "Ajax" обязана своим происхождением этому формату. Я не собираюсь подробно обсуждать XML и лишь замечу, что этот язык страдает избыточностью синтаксиса, трудно читается и требует относительно больших затрат времени и ресурсов для генерации и обработки данных.

В последние годы формат XML был заметно потеснен форматом JSON (JavaScript Object Notation), отличающимся простотой и исключительной приспособленностью для работы с JavaScript-кодом (о чем говорит уже само его название). Специально для этого примера я создал файл mydata.json и сохранил его вместе с файлом test.html на веб-сервере. Содержимое файла mydata.json представлено в примере ниже:

[{"name":"Астра","product":"astor","stocklevel":"10","price":"2.99"},
 {"name":"Нарцисс","product":"daffodil","stocklevel":"12","price":"1.99"},
 {"name":"Роза","product":"rose","stocklevel":"2","price":"4.99"},
 {"name":"Пион","product":"peony","stocklevel":"0","price":"1.50"},
 {"name":"Примула","product":"primula","stocklevel":"1","price":"3.12"},
 {"name":"Подснежник","product":"snowdrop","stocklevel":"15","price":"0.99"}]

В этом файле содержатся данные для различных видов цветочной продукции, и несложно заметить, что форма представления данных JSON совпадает с формой представления данных, встраиваемых в JavaScript-код. Это и есть одна из причин, по которым формат JSON вытеснил XML в веб-приложениях.

Для загрузки и обработки этих данных с помощью Ajax можно использовать метод get(), как показано в примере ниже (здесь мы используем шаблоны данных, вам потребуется добавить ссылку на библиотеку jquery.tmpl.js):

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="jquery.tmpl.min.js"></script>
<script>	
$(function() {
	
	$('<button>Ajax</button>').appendTo('#buttonDiv').click(function(e) {
		$.get('mydata.json', function(data) {
			 var template = $('#flowerTmpl');
			 template.tmpl(data.slice(0, 3)).appendTo('#row1');
			 template.tmpl(data.slice(3)).appendTo('#row2');
		});
		
		e.preventDefault();
	});    
		
});
</script>
<script id="flowerTmpl" type="text/x-jquery-tmpl">
        <div class="dcell">
            <img src="http://professorweb.ru/downloads/jquery/${product}.png"/>
            <label for="${product}">${name}:</label>
            <input name="${product}" data-price="${price}" data-stock="${stocklevel}"
                value="0" required />
        </div>
</script>

В этом примере файл с данными JSON запрашивается с сервера после щелчка на кнопке. Полученные данные передаются функции так, как если бы это был обычный HTML-фрагмент. Для обработки данных и генерации из них HTML-элементов используется подключаемый модуль шаблонов, а для вставки элементов в документ — методы slice() и appendTo(). Обратите внимание на то, что мы не предпринимаем никаких действий для того, чтобы преобразовать строку JSON в объект JavaScript — за нас это автоматически делает jQuery.

Некоторые веб-серверы (например Microsoft IIS 7.5) не возвращают содержимое браузеру, если не могут распознать расширение имени файла или формат данных. Чтобы этот пример работал с IIS, мне пришлось установить новое соответствие между расширением имени файла (.json) и MIME-типом данных в формате JSON (application/json). Пока я этого не сделал, веб-сервер IIS отвечал на мой запрос файла mydata.json сообщением с кодом 404 ("Not Found").

Передача данных GET-запросам

Данные могут пересылаться на сервер в составе GET-запросов, для отправки которых можно использовать методы get(), load(), getScript() и getJSON(). Соответствующий пример приведен ниже:

$(function() {
	
	var requestData = {
		country: "RU",
		city: "Moscow"
	}
	
	$('<button>Ajax</button>').appendTo('#buttonDiv').click(function(e) {
		$.get('mydata.json', requestData, function(data) {
			 var template = $('#flowerTmpl');
			 template.tmpl(data.slice(0, 3)).appendTo('#row1');
			 template.tmpl(data.slice(3)).appendTo('#row2');
		});
		
		e.preventDefault();
	});    
		
});

Предоставленные вами данные присоединяются к указанному URL-адресу в виде строки запроса. Это означает, что запрос имеет примерно следующий вид:

localhost/web/jquery/mydata.json?country=RU&city=Moscow

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

Выполнение POST-запросов Ajax

Теперь, когда вы уже знаете, как получать данные с сервера, можно перейти к рассмотрению вопроса о способах отправки данных, точнее — отправки данных формы на сервер. Для решения этой задачи также предусмотрен прямой метод post(), значительно упрощающий отправку данных формы. Прежде чем перейти к рассмотрению данного метода, вы должны настроить свой сервер.

Серверный сценарий

Для этого раздела вам понадобится серверный сценарий, который будет получать данные, отправленные браузером с использованием HTTP-метода POST, выполнять простую операцию с использованием этих данных и генерировать ответ. Сценарий PHP (вы можете написать собственный сценарий для своей платформы) для этого раздела приведен в примере ниже:

<?php 	
	$total = 0;
	$arr = array();
	
	foreach($_POST as $key=>$value)
	{
  		if ($value > 0)
		{
			$total += $value;
			$arr[$key] = $value;
		}
	}
	$arr['total'] = $total;
	
	header('Content-Type: application/json');
	echo json_encode($arr);
?>

Сохраните сценарий в файле phphandler.php. Этот сценарий обрабатывает данные, отправленные браузером, и генерирует ответ в формате JSON. Вообще говоря, можно было сделать так, чтобы этот сценарий возвращал HTML-данные, но формат JSON более компактен, и во многих случаях с ним проще работать.

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

{"astor":"1","daffodil":"2","rose":"2","total":5}

Ранее формат JSON использовался нами для представления массива объектов, тогда как данный серверный сценарий возвращает одиночный объект, свойства которого соответствуют выбранным видам цветов. Свойство total содержит общее количество выбранных цветов. Должен признать, что подобного рода операции слишком просты для того, чтобы дать полное представление о возможностях обработки данных на сервере, но все же основной предмет нашего рассмотрения — Ajax, а не вопросы разработки серверных приложений.

Использование метода POST для отправки данных формы

Итак, теперь, когда вы уже имеете подготовленный сервер, можем приступить к использованию метода post() для оправки данных формы на сервер, как показано в примере ниже:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="jquery.tmpl.min.js"></script>
<script>	
$(function() {
	
    // При загрузке документа отключаем кнопку отправки формы
	$('button').get(0).disabled = true;
	
    // Загружаем из файла mydata.json базовый код
	$.getJSON('mydata.json', function(data) {
			 var template = $('#flowerTmpl');
			 template.tmpl(data.slice(0, 3)).appendTo('#row1');
			 template.tmpl(data.slice(3)).appendTo('#row2');
             
             // После построения документа активируем кнопку отправки формы
			 $('button').get(0).disabled = false;
		});
		
	// Обработчик клика по кнопке "Заказать"
	$('button').click(function(e) {
        // Сериализуем данные формы для отправки серверу
        var formData = $('form').serialize();
		
        // Ajax-запрос серверу типа POST
        $.post('phphandler.php', formData, 
		       function(data) {
                   // Вызываем вспомогательную функцию
			       processServerResponse(data);
		       });
               
		// Отменяем прямую отправку формы
		e.preventDefault()
    });
	
	function processServerResponse(data) {
        // Изначально скрываем всю продукцию
        var inputElems = $('div.dcell').hide();                
        
        for (var prop in data) {
            // Отображаем только ту продукцию, заказ которой больше 0
            // (в ответе от сервера содержится только такая продукция)
            var filtered = inputElems.has('input[name=' + prop + ']')
                .appendTo("#row1").show();
        }
        
        // Скрываем базовые элементы формы
        $('#buttonDiv, #totalDiv').remove();
        
        // Отображаем новые элементы из шаблона totalTmpl
        $('#totalTmpl').tmpl(data).appendTo('body');
    }
		
});
</script>
<script id="totalTmpl" type="text/x-jquery-tmpl">
        <div id="totalDiv" style="clear: both; padding: 5px">
            <div style="text-align: center">Всего заказов: <span id=total>${total}</span></div>
            <div id="buttonDiv"><button type="submit">Заказать</button></div>                    
        </div>
</script>
<script id="flowerTmpl" type="text/x-jquery-tmpl">
        <div class="dcell">
            <img src="http://professorweb.ru/downloads/jquery/${product}.png"/>
            <label for="${product}">${name}:</label>
            <input name="${product}" data-price="${price}" data-stock="${stocklevel}"
                value="0" required />
        </div>
</script>

Этот пример кажется немного сложнее, чем он есть на самом деле. Мы начинаем с того, что используем метод getJSON() для получения файла mydata.json, содержащего описание цветочной продукции, после чего генерируем элементы с помощью шаблона данных и добавляем их в документ. В результате мы оказываемся на уже хорошо вам знакомой исходной позиции, которую, надеюсь, вы уже успели полюбить:

Исходная позиция перед отправкой данных на сервер

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

Первое, что делает обработчик — вызывает метод serialize() для элемента form. Это весьма полезный метод, который последовательно обрабатывает все элементы формы и создает строку, закодированную для передачи на сервер в качестве фрагмента URL-адреса. Для введенных мною значений метод serialize() генерирует следующую строку:

astor=12&daffodil=20&rose=0&peony=0&primula=0&snowdrop=6

Я использую здесь метод serialize(), поскольку метод post() отправляет данные в закодированном для передачи в составе URL формате. Получив объект data, созданный на основе значений элементов input, мы вызываем метод post(), чтобы инициировать Ajax-запрос.

В качестве аргументов метод post() получает URL-адрес, по которому должны быть отправлены данные, подлежащие отправке, сами данные и функцию, которая должна быть вызвана в случае успешного выполнения запроса.

В этом примере получаемый от сервера ответ передается функции processServerResponse(). Сначала мы скрываем элементы div уровня ячеек, формирующие табличную компоновку страницы с использованием стилей CSS (они относятся к классу dcell), а затем отображаем те из них, которые соответствуют свойствам объекта JSON, полученного от сервера. Кроме того, мы используем шаблон данных для генерации разметки, отображающей общее количество выбранных единиц продукции. Это все можно было бы сделать с помощью клиента, но наша цель — научиться обрабатывать данные, возвращаемые POST-запросом Ajax. Результат показан на рисунке:

Результат обработки данных, возвращаемых сервером в ответ на POST-запрос Ajax

Теперь вы сами могли убедиться в том, насколько легко отправить данные формы на сервер (и, конечно же, насколько просто обработать ответ, особенно если он возвращается в формате JSON).

Указание ожидаемого типа данных

При использовании методов get() и post() библиотеке jQuery приходится определять тип данных, получаемых от сервера в ответ на запрос. Данными может быть все что угодно, начиная от HTML-кода и заканчивая файлами JavaScript. Для определения типа данных библиотека jQuery использует содержащуюся в ответе информацию, и в частности — заголовок Content-Type. Как правило, этого вполне достаточно, но иногда jQuery приходится оказывать небольшую помощь. Обычно необходимость в этом возникает из-за указания сервером неверного MIME-типа в ответе.

Можно изменить информацию, поставляемую сервером, и сообщить jQuery, какой тип данных ожидается, передавая методам get() и post() дополнительную информацию. Аргумент может принимать одно из следующих значений:

В примере ниже показано, как задать ожидаемый тип данных для метода post():

...
   $('button').click(function(e) {
        var formData = $('form').serialize();
		
		$.post('phphandler.php', formData, 
		       function(data) {
			       processServerResponse(data);
		       }, "json");
		
		e.preventDefault()
    });
    ...

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

<?php 
	...
	
	// header('Content-Type: application/json');
	echo json_encode($arr);
?>

Ловушка при работе с Ajax

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

$(function() {
	
    $('<button>Ajax</button>').appendTo('#buttonDiv').click(function(e) {
        e.preventDefault();
        
        var elems;
        
        $.get("flowers.html", function(data) {           
            elems = $(data).filter("div").addClass("dcell");
        });
        
        elems.slice(0, 3).appendTo('#row1');
        elems.slice(3).appendTo("#row2");
        
    });
		
});

В этом сценарии определяется переменная elems, которая используется в функции обратного вызова Ajax для сохранения результата выполнения запроса к серверу. Полученные с сервера элементы добавляются в документ с помощью методов slice() и appendTo(). Если вы выполните этот пример, то увидите, что ни один из элементов не будет добавлен в документ, и вместо этого на консоли отобразится сообщение, конкретный текст которого зависит от типа браузера. Ниже приведено сообщение, отображаемое на консоли браузера Google Chrome:

Ошибка вызова метода на несуществующем объекте jQuery, в следствие работы с Ajax

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

  1. Определяется переменная elems.

  2. Получаемые с сервера данные присваиваются переменной elems.

  3. Элементы извлекаются из переменной elems и добавляются в документ.

В действительности происходит следующее:

  1. Определяется переменная elems.

  2. Запускается асинхронный запрос к серверу.

  3. Элементы извлекаются из переменной elems и добавляются в документ.

При этом в какой-то промежуточный момент времени вскоре после отправки браузером запроса происходит следующее:

  1. В браузер поступают данные от сервера.

  2. Данные обрабатываются и присваиваются переменной elems.

Причина появления сообщения об ошибке — вызов метода slice() для переменной в тот момент, когда она еще не содержит никаких элементов. Хуже всего то, что иногда этот код может работать правильно. Объясняется это тем, что Ajax-запрос выполняется настолько быстро, что к тому времени, когда начинается обработка переменной, в ней уже содержатся ожидаемые данные (обычно это наблюдается в тех случаях, когда данные кешируются браузером или когда между отправкой Ajax-запроса и попыткой обработки данных выполняются какие-либо сложные операции). Так что теперь, если ваш код будет вести себя подобным образом, вы уже знаете, что может быть причиной его необычного поведения.

Использование вспомогательных методов для работы с конкретными типами данных

Библиотека jQuery предоставляет три вспомогательных метода, которые делают работу с некоторыми типами данных более удобной. Некоторые из них мы рассмотрим далее.

Получение HTML-фрагментов

Метод load() предназначен для получения только HTML-данных, что позволяет совместить запрос HTML-фрагмента, обработку ответа от сервера для создания набора элементов и вставку этих элементов в документ в одном действии. Пример использования метода load() представлен ниже:

$(function() {
	
    $('<button>Ajax</button>').appendTo('#buttonDiv').click(function(e) {
        $('#row1').load("flowers.html");
        e.preventDefault();        
    });
		
});

В этом сценарии мы вызываем метод load() для элемента, в который хотим вставить новые элементы, и передаем ему URL-адрес в качестве аргумента. Если запрос завершается успешно, а полученный от сервера ответ содержит действительный HTML-фрагмент, элементы вставляются в указанное место в документе, как показано на рисунке:

Добавление элементов в документ с помощью метода load()

Вы видите, что все элементы из файла flowers.html добавлены в документ, как мы и хотели, но поскольку у них отсутствует атрибут class, то они не укладываются в табличную компоновку страницы, используемую в основном документе. Поэтому метод load() наиболее полезен в тех случаях, когда все элементы могут быть вставлены в одно место в документе без какой-либо дополнительной обработки.

Получение и выполнение сценариев

Метод getScript() загружает файл JavaScript, а затем выполняет содержащиеся в нем инструкции. Чтобы продемонстрировать работу этого метода, я создал файл myscript.js и сохранил его вместе с файлом test.html на своем веб-сервере. Содержимое этого файла представлено в примере ниже:

// myscript.js
var flowers = [
    ["astor", "daffodil", "rose"],
    ["peony", "primula", "snowdrop"],
    ["carnation", "lily", "orchid"]
]

var flowersR = [
    ["Астра", "Нарцисс", "Роза"],
    ["Пион", "Примула", "Подснежник"],
    ["Гвоздика", "Лилия", "Орхидея"]
]

$('<div id=row3 class=drow/>').appendTo('div.dtable');

var fTemplate = $('<div class=dcell><img/><label/><input/></div>');

for (var row = 0; row < flowers.length; row++) {
    var fNames = flowers[row];
    var fNamesR = flowersR[row];
    
    for (var i = 0; i < fNames.length; i++) {
        fTemplate.clone().appendTo("#row" + (row + 1)).children()
            .filter('img').attr('src',"http://professorweb.ru/downloads/jquery/" 
                                          + fNames[i] + ".png").end()
            .filter('label').attr('for', fNames[i]).text(fNamesR[i]).end()
            .filter('input').attr({name: fNames[i], value: 0})
    }        
}

Эти инструкции генерируют три ряда элементов, описывающих цветы. Мы обошлись здесь без определения шаблонов и использовали циклы для генерации элементов (хотя, вообще говоря, следовало бы воспользоваться шаблонами данных).

Самое важное, что необходимо знать при работе со сценариям, — между инициализацией Ajax-запроса и выполнением инструкций сценария состояние документа может измениться. В примере ниже приведен сценарий из основного документа, в котором по-прежнему используется метод getScript(), но при этом, еще до завершения Ajax-запроса, модифицируется дерево DOM:

$(function() {
	
    $('<button>Ajax</button>').appendTo('#buttonDiv').click(function(e) {
           $.getScript("myscript.js");
           $('#row2').remove();
                
           e.preventDefault();          
    });
		
});

Здесь мы вызываем метод getScript() для основной функции $() и передаем ему в качестве аргумента URL-адрес файла, который хотим использовать. Если сервер способен предоставить указанный файл и этот файл содержит действительный JavaScript-код, то последний будет выполнен.

Метод getScript() можно использовать для загрузки любых сценариев, но особенно полезно использовать его для загрузки и выполнения таких сценариев, как сценарии для отслеживания статистики посещения сайтов или определения географического местоположения клиента, которые не связаны с поддержкой основной функциональности веб-приложения. Пользователя не особенно заботит, в состоянии ли мы точно определять его местоположение для ведения статистики, тогда как длительное ожидание загрузки и выполнения сценария будет действовать ему на нервы.

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

В данном примере после запуска Ajax-запроса с помощью метода getScript() из документа удаляется элемент row2, для чего используется метод remove(). Данный элемент используется в файле myscript.js для вставки новых элементов. Эти элементы отбрасываются незаметным для пользователя образом, поскольку в документе селектору #row2 ничто не соответствует. Итоговый результат представлен на рисунке:

Результат внесения изменений в документ во время выполнения запроса

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

Получение данных в формате JSON

Для загрузки данных JSON с сервера предназначен метод getJSON(). Возможно, это наименее полезный из всех трех вспомогательных методов, поскольку он не делает с данными ничего сверх того, что делает базовый метод get().

Пройди тесты
Лучший чат для C# программистов