AJAX
Лечим JavaScript зависимость
Любовь к AJAX бывает чрезмерной, и в погоне к Web2.0 (3.0, 4.0, … — желаемое подчерк-нуть) мы создаём сайты в которых все наши действия бегут через XMLHTTPRequest. Нет, это конечно не плохо — снижаем нагрузку на сервер, канал и т.д. и т.п., но есть одно "но" — у нас есть поисковые машины, которые не озадачивают себя выполнением JavaScript кода, а контент, спрятанный за AJAX запросом, им отдать всё таки нужно. Следовательно, у нас возникает необходимость дублирования навигации (это как минимум) для клиентов без JavaScript.
Стоит помнить, что есть ещё пользователи, у которых отключён JavaScript в браузере (или даже не поддерживается, привет тебе, рысь), но эти знают, что делают. А есть ещё скрипты, которые ломаются, и не дают обычным пользователям воспользоваться навигацией по сайту, а пользователей это очень сильно расстраивает, так что эта глава не "просто так".
Как же всё это обойти и на грабли не наступить? Да всё очень просто — создавайте обычную навигацию, которую вы бы делали не слышав ни разу о AJAX и компании:
<ul class="navigation"> <li><a href="/">Home</a></li> <li><a href="/about.html">About Us</a> </li> <li><a href="/contact.html">Contact Us</a></li> </ul> <section id="content"><!-- Content --></section>
Данный пример работает у нас без JavaScript’a, все страницы в нашем меню используют один и тот же шаблон для вывода информации, и по факту у нас изменяется лишь содержимое <div> с "id="content"". Теперь приступим к загрузке контента посредством AJAX – для этого добавим следующий код:
$(function() { // вешаем обработчик на все ссылки в нашем меню navigation $("ul.navigation a").click(function(){ var url = $(this).attr("href"); // возьмем ссылку url =+ "?ajax=true"; // добавим к ней параметр ajax=true $("#content").load(url); // загружаем обновлённое содержимое return false; // возвращаем false // - дабы не сработал переход по ссылке }); });
В данном примере мы предполагаем, что сервер, видя параметр "ajax=true" вернет нам не полностью всю страницу, а лишь обновление для искомого элемента <div id="content">.
Конечно, сервер должен быть умнее и не требовать явного указания для использования AJAX’а, а должен вполне удовлетвориться, словив header "X_REQUESTED_WITH" со значением "XMLHttpRequest". Большинство современных фреймворков для web-разработки с этим справляются "из коробки".
Если же управлять поведением сервера проблематично, и он упёрто отправляет нам всю страницу целиком, то можно написать следующий код:
$(function() { // вешаем обработчик на все ссылки в нашем меню navigation $("ul.navigation a").click(function(){ var url = $(this).attr("href"); // возьмем ссылку // загружаем страницу целиком, но в наш контейнер вставляем // лишь содержимое #content загружаемой страницы $("#content").load(url + " #content > *"); return false; // возвращаем false }); });
Если в подгружаемом содержимом так же есть ссылки – то вы уже должны знать как "оживить" события.
Прокачиваем AJAX
У нас есть три способа для "прокачки" AJAX'а в jQuery: это создание префильтров, добавление новых конверторов и транспортов.
Префильтры
Префильтр – это функция, которая будет вызвана до "ajaxStart", в ней вы сможете изменить как объект "jqXHR", так и любые сопутствующие настройки:
// регистрация AJAX префильтра $.ajaxPrefilter(function( options, originalOptions, jqXHR ) { // наши манипуляции над настройками и jqXHR });
Для чего всё это? Да вот простая задачка – не ждать "старый" AJAX ответ, если мы запрашиваем URL заново:
// коллекция текущих запросов var currentRequests = {}; $.ajaxPrefilter(function( options, originalOptions, jqXHR ) { // наша произвольная настройка if ( options.abortOnRetry ) { if ( currentRequests[ options.url ] ) { // отменяем старый запрос currentRequests[ options.url ].abort(); } currentRequests[ options.url ] = jqXHR; } }); // вызов с использованием фильтра $.ajax({ /* ... */ abortOnRetry: true })
Ещё можно изменить опции вызова, вот пример который по флагу "crossDomain" пересылает запрос на заранее подготовленную проксирующую страницу на нашем сервере:
$.ajaxPrefilter(function( options ) { if ( options.crossDomain ) { options.url = "/proxy/" + encodeURIComponent( options.url ); options.crossDomain = false; } });
Префильтры можно "вешать" на определенный тип "dataType" (т.е. в зависимости от ожидаемого типа данных от сервера будут срабатывать различные фильтры):
$.ajaxPrefilter("json script", function(options, original, jqXHR) { /* ... */ });
Ну и последнее, для переключение "dataType" на какой-нить другой нам достаточно будет вернуть необходимое значение:
$.ajaxPrefilter(function( options ) { // это наша функция-детектор необходимых URL if ( isActuallyScript( options.url ) ) { // теперь "ждём" script return "script"; } });
Будьте очень осторожны когда оперируете глобальными настройками, да ещё через такую неявную фичу как фильтры – задокументируйте подобные подходы в сопроводительной документации, иначе разработчики которые будут в дальнейшем сопровождать ваш код будут сильно ругаться (в качестве оных можете оказаться и вы сами, ну через пару месяцев)
Конверторы
Конвертор – функция обратного вызова, которая вызывается в том случае, когда полученный типа данных не совпадает с ожидаемым (т.е. "dataType" указан неверно). Всё конверторы хранятся в глобальных настройках "ajaxSettings":
// формат ключа "из_формата в_формат" // в качестве входного формата можно использовать "*" converters: { "* text": window.String, // что угодно приводим к тексту "text html": true, // текст к html (флаг true == без изменений) "text json": jQuery.parseJSON, // текст к JSON "text xml": jQuery.parseXML // разбираем текст как xml }
Для расширения набора конверторов потребуется функция "$.ajaxSetup()":
$.ajaxSetup({ converters: { "text mydatatype": function( textValue ) { if ( valid( textValue ) ) { // разбор пришедших данных return mydatatypeValue; } else { // возникла ошибка throw exceptionObject; } } } });
Имена "dataType" должны всегда быть в нижнем регистре
Конверторы следует использовать, если требуется внедрить произвольные форматы "dataType", или для конвертации данных в нужный формат. Необходимый "dataType" указываем при вызове метода "$.ajax()":
$.ajax( url, { dataType: "mydatatype" });
Конверторы можно задавать так же непосредственно при вызове "$.ajax()", дабы не засорять общие настройки:
Конверторы можно задавать так же непосредственно при вызове "$.ajax()", дабы не засорять общие настройки: $.ajax( url, { dataType: "xml text mydatatype", converters: { "xml text": function( xmlValue ) { // получаем необходимые данные из XML return textValue; } } });
Чуть-чуть пояснений – мы запрашиваем "XML", который конвертируем в текст, который будет передан в наш конвертор из "text" в "mydatatype"
Транспорт
Использование своего транспорта – это крайняя мера, прибегайте к ней, только в том случае если с поставленной задачей нельзя справиться с использованием префильтров и конверторов
Транспорт – это объект, который предоставляет два метода – "send()" и "abort()" – они будут использоваться внутри метода "$.ajax()". Для регистрации своего метода транспортировки следует использовать метод "$.ajaxTransport()", будет это выглядеть как-то так:
$.ajaxTransport( function( options, originalOptions, jqXHR ) { if ( /* transportCanHandleRequest */ ) { return { send: function( headers, completeCallback ) { /* отправляем запрос */ }, abort: function() { /* отменяем запрос */ } }; } });
Проясню чуток параметры с которыми будем работать:
- options – настройки запроса (то что указываем при вызове "$.ajax()")
- originalOptions – "чистые" настройки, даже без учёта изменений "по умолчанию"
- jqXHR – объект "jQuery XMLHttpRequest"
- headers – заголовки запроса в виде связки ключ-значение
- completeCallback – функция обратного вызова, её следует использовать для оповещения о завершении запроса
Функция "completeCallback" имеет следующую сигнатуру:
function ( status, statusText, responses, headers ) { /* какой-то код */ }
где:
- status – HTTP статус ответа.
- statusText – текстовая интерпретация ответа
- responses (опционально) – это объект содержащий ответы сервера во всех форматах, которые поддерживает транспорт, для примера: родной "XMLHttpRequest" будет выглядеть как "{ xml: XMLData, text: textData }" при запросе XML документа
- headers (опционально) – строка содержащие заголовки ответа сервера, ну если конечно транспорт может их получить (вот например метод "XMLHttpRequest.getAllResponseHeaders()" может).
Как и префильтры, транспорт можно привязывать к определенному типу запрашиваемых данных:
$.ajaxTransport( "script", function( options, originalOptions, jqXHR ) { /* привязываемся лишь к script*/ });
А теперь мега-напряг – пример транспорта "image":
$.ajaxTransport( "image", function( options ) { if (options.type === "GET" && options.async ) { var image; return { send: function( _ , callback ) { image = new Image(); function done( status ) { // подготовим if ( image ) { var statusText = ( status == 200 ) ? "success" : "error", tmp = image; image = image.onreadystatechange = image.onerror = image.onload = null; callback( status, statusText, { image: tmp } ); } } image.onreadystatechange = image.onload = function() { done( 200 ); }; image.onerror = function() { done( 404 ); }; image.src = options.url; }, abort: function() { if ( image ) { image = image.onreadystatechange = image.onerror = image.onload = null; } } }; // /return } // /if }); // /ajaxTransport
Рабочий пример вы увидите ниже, но я хотел бы ещё раз напомнить, что это "advanced level", и данный раздел лишний в учебнике "для начинающих".
<!DOCTYPE html> <html dir="ltr" lang="en-US"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Пример создания транспорта для AJAX</title> <link rel="profile" href="http://gmpg.org/xfn/11"/> <link rel="shortcut icon" href="http://anton.shevchuk.name/favicon.ico"/> <link rel="stylesheet" href="css/styles.css"/> <script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/code.js"></script> <script> $.ajaxTransport("image", function (options) { if (options.type === "GET" && options.async) { var image; return { send:function (_, callback) { image = new Image(); // подготовим функцию done function done(status) { if (image) { var statusText = ( status == 200 ) ? "success" : "error", tmp = image; image = image.onreadystatechange = image.onerror = image.onload = null; callback(status, statusText, { image:tmp }); } } image.onreadystatechange = image.onload = function () { done(200); }; image.onerror = function () { done(404); }; image.src = options.url; }, abort:function () { if (image) { image = image.onreadystatechange = image.onerror = image.onload = null; } } }; } }); </script> </head> <body> <div id="content" class="wrapper box"> <menu> <a href="index.html" title="go prev" class="button alignleft" rel="prev">← Back</a> <a href="#" title="reload" class="button alignleft" onclick="window.location.reload();return false">Reload ¤</a> <hr/> <pre><code contenteditable="true">$.ajax(<span>'images/events.png'</span>, { dataType:<span>'image'</span>, success: function(data) { $(<span>'article'</span>).html(data); } });</code></pre> <button type="button" class="code">Run Code</button> </menu> <header> <h1>Своя реализация AJAX-транспорта</h1> <h2>Пример надуманный</h2> </header> <article> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rutrum, lectus eu varius consectetur, libero velit hendrerit augue, ut posuere enim neque in libero. Donec eget sagittis nibh. Suspendisse sed tincidunt urna. Cras quis euismod neque. Maecenas auctor ultricies posuere. Pellentesque luctus pulvinar dui eget semper. Donec sodales odio eu sapien varius luctus. Donec dictum feugiat diam at malesuada. Sed nec massa in augue condimentum faucibus quis ut diam. Quisque nisl sem, semper nec vulputate vel, mattis sit amet justo. Aliquam purus felis, tempor at scelerisque quis, tincidunt in neque. Etiam ut risus diam. Pellentesque fermentum risus id elit feugiat cursus. Ut fringilla dictum diam, sed iaculis lorem pulvinar ut. Cras vel elit id velit commodo viverra sit amet vel orci. </p> </article> <footer> ©copyright 2014 Anton Shevchuk — <a href="http://anton.shevchuk.name/jquery-book/">jQuery Book</a> </footer> <script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-1669896-2']); _gaq.push(['_trackPageview']); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script> </div> </body> </html>
По следам официальной документации:
- "Extending Ajax: Prefilters, Converters, and Transports" [http://api.jquery.com/extending-ajax/]