18 Работа с протоколом HTTP

Протокол передачи гипертекста (Hypertext Transfer Protocol, HTTP) определяет, как веб-браузеры должны запрашивать документы, как они должны передавать информацию веб-серверам и как веб-серверы должны отвечать на эти запросы и передачи. Очевидно, что веб-браузеры очень много работают с протоколом HTTP. Тем не менее, как правило, сценарии не работают с протоколом HTTP, когда пользователь щелкает на ссылке, отправляет форму или вводит URL в адресной строке

Однако JavaScript-код способен работать с протоколом HTTP. HTTP-запросы могут инициироваться, когда сценарий устанавливает значение свойства location объекта Window или вызывает метод submit() объекта Form. В обоих случаях браузер загружает в окно новую страницу. Такого рода взаимодействие с протоколом HTTP может быть вполне оправданным в веб-страницах, состоящих из нескольких фреймов, но в этой главе мы будем говорить совсем о другом. Здесь мы рассмотрим такое взаимодействие JavaScript-кода с веб-сервером, при котором веб-браузер не перезагружает содержимое окна или фрейма.

Архитектуру веб-приложений, отличительной чертой которых является работа с протоколом HTTP, описывает термин Ajax.

Ajax – это аббревиатура от Asynchronous JavaScript and XML (асинхронный JavaScript и XML). Этот термин предложил Джесси Джеймс Гаррет (Jesse James Garrett) и впервые использовал его в своей статье «Ajax: A New Approach to Web Applications» в феврале 2005 года. (http://www.adaptivepath.com/publications/essays/archives/000385.php) (От переводчика: перевод этой статьи на русский язык можно найти по адресу http://galleo.ru/articles/wl60.). В течение многих лет термин «Ajax» был громким словом, которое употребляли к месту и не к месту, а сейчас это всего лишь удобный термин, обозначающий архитектуру веб-приложения, опирающегося в своей работе на HTTP-запросы.
Ключевой особенностью Ajax-приложения является использование протокола HTTP для инициации обмена данными с веб-сервером без необходимости перезагружать страницу. Возможность избежать перезагрузки страницы (что было привычным на первых этапах развития Всемирной паутины) позволяет создавать веб-приложения, близкие по своему поведению к обычным приложениям. Веб-приложение может использовать технологии Ajax для передачи на сервер результатов взаимодействия с пользователем или для ускорения запуска приложения, отображая сначала простую страницу и подгружая дополнительные данные и компоненты страницы по мере необходимости.

Похожую архитектуру веб-приложений, также использующих протокол HTTP.1, описывает термин Comet. В некотором смысле архитектура Comet является обратной по отношению к Ajax: в архитектуре Comet не клиент, а сервер инициирует взаимодействие, асинхронно отсылая сообщения клиенту. Если веб-приложению потребуется отвечать на сообщения, отправляемые сервером, оно сможет использовать приемы Ajax для отправки или запроса данных. В архитектуре Ajax клиент «вытягивает» данные с сервера. В архитектуре Comet сервер «навязывает» данные клиенту. Иногда архитектуру Comet называют «Server Push», «Ajax Push» и «HTTP Streaming».

Название Comet было предложено Алексом Расселом (Alex Russell) в статье «Comet: Low Latency Data for the Browser» http://infrequently.org/2006/03/comet-low-latency-data-for-the-browser/). Вероятно, выбирая его, Алекс Рассел хотел обыграть термин Ajax: дело в том, что в США Comet и Ajax являются названиями чистящих средств)

Есть множество способов реализации архитектур Ajax и Comet, и эти базовые реализации иногда называют транспортами. Элемент <img>, например, имеет свойство src. Когда сценарий записывает в это свойство URL-адрес, инициируется HTTP-запрос GET и выполняется загрузка содержимого с этого URL-адреса. Сценарий может таким способом отправлять информацию веб-серверу, добавляя ее в виде строки запроса в URL-адрес изображения и устанавливая свойство src элемента <img>. В ответ на этот запрос веб-сервер должен вернуть некоторое изображение, которое, например, может быть невидимым: прозрачным и размером 1x1 пиксел.

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

Элемент <img> – не самый лучший транспорт Ajax, потому что обмен данными ведется только в одном направлении: клиент может передать данные серверу, но ответом сервера всегда будет изображение, извлечь информацию из которого на стороне клиента очень непросто. Элемент <iframe> обладает большей гибкостью. При использовании элемента <iframe> в качестве транспорта Ajax сценарий сначала добавляет в URL-адрес информацию, предназначенную для веб-сервера, а затем записывает этот URL-адрес в свойство src тега <iframe>. Сервер создает HTML- документ, содержащий ответ на запрос, и отправляет его обратно веб-браузеру, который выводит ответ в теге <iframe>. При этом элемент необязательно должен быть видимым для пользователя – он может быть сокрыт, например, средствами таблиц стилей CSS. Сценарий может проанализировать ответ сервера, выполнив обход документа в элементе <iframe>. Обратите внимание, что взаимодействие с документом ограничивается политикой общего происхождения, о которой рассказывается в разделе 13.6.2.

Даже элемент <script> имеет свойство src, которое может установлено на инициирование HTTP-запроса GET. Использование элементов <script> для работы с протоколом HTTP выглядит особенно привлекательно, потому что они не являются субъектами политики общего происхождения и могут использоваться для взаимодействий с разными серверами. Обычно с основанным на элементе <script> транспортом Ajax ответ сервера имеет вид данных в формате JSON (раздел 6.9), которые автоматически «декодируются», когда содержимое элемента <script> выполняется интерпретатором JavaScript. Из-за использования формата данных JSON этот транспорт Ajax получил название «JSONP».

Хотя архитектура Ajax может быть реализована поверх транспорта <iframe> или <script>, существует более простой путь. Уже достаточно давно все браузеры стали поддерживать объект XMLHttpRequest, определяющий прикладной интерфейс для работы с протоколом HTTP. Этот интерфейс обеспечивает возможность выполнять POST-запросы в дополнение к обычным GET-запросам, и может возвращать ответ веб-сервера синхронно или асинхронно в виде простого текста или в виде объекта Document. Несмотря на свое название, объект XMLHttpRequest не ограничивается использованием XML-документов – он в состоянии принимать любые текстовые документы. Прикладной интерфейс объекта XMLHttpRequest рассматривается в разделе 18.1, который занимает значительную часть главы. Большая часть примеров реализации архитектуры Ajax в этой главе в качестве транспорта использует объект XMLHttpRequest, но в разделе 18.2 будет также показано, как использовать транспорт на основе элемента <script>, так как он способен обходить ограничения политики общего происхождения.

Транспортные механизмы в архитектуре Comet сложнее, чем в архитектуре Ajax, но все они требуют установления (и восстановления, в случае необходимости) соединения с сервером со стороны клиента и обязывают сервер поддерживать соединение открытым, чтобы через него можно было отправлять асинхронные сообщения. Транспортом в архитектуре Comet может служить, например, скрытый элемент <iframe>, если сервер отправляет сообщения в виде элементов <script>, выполняемых в элементе <iframe>. Более надежный и переносимый подход к реализации архитектуры Comet заключается в устанавлении соединения клиента с сервером (посредством Ajax-транспорта) и поддержании сервером этого соединения открытым, пока остается необходимость отправлять сообщения. Каждый раз, когда сервер отправляет сообщение, он закрывает соединение, что позволяет гарантировать благополучное получение сообщения клиентом. После обработки сообщения клиент может сразу же установить новое соединение для будущих сообщений.

Реализация надежного и переносимого транспорта для архитектуры Comet является сложной задачей, и большинство веб-разработчиков, использующих архитектуру Comet, опираются на транспорты, реализованные в веб-фреймворках, таких как Dojo. На момент написания этих строк производители браузеров приступили к реализации положений проекта спецификации «Server-Sent Events», связанной со стандартом HTML5, которая определяет простой прикладной интерфейс для архитектуры Comet в виде объекта EventSource. Интерфейс объекта EventSource будет рассматриваться в разделе 18.3, и там же будет продемонстрирована простая его имитация на основе объекта XMLHttpRequest.

Имеется также возможность конструировать высокоуровневые протоколы взаимодействий поверх Ajax и Comet. Эти приемы организации взаимодействий типа клиент/сервер можно использовать, например, в качестве основы механизма RPC (Remote Procedure Call – вызов удаленных процедур) или системы событий типа издатель/подписчик. Однако в данной главе мы не будем рассматривать подобные высокоуровневые протоколы, а сконцентрируемся на прикладных интерфейсах поддержки архитектур Ajax и Comet.

Использование XML не является обязательным

Символ «X» в аббревиатуре «Ajax» обозначает «XML». Основной прикладной интерфейс для работы с протоколом HTTP на стороне клиента (XMLHttpRequest) также содержит название «XML» в своем имени, и, как мы узнаем далее, одно из свойств объекта XMLHttpRequest имеет имя responseXML. Вследствие этого может сложиться впечатление, что XML является важной частью работы с протоколом HTTP. Но это не так: эти имена являются историческим наследием тех дней, когда XML было модным словечком. Конечно, реализации Ajax способны работать с XML-документами, но использование формата XML не является обязательным, и в действительности он редко используется не практике. Спецификация XMLHttpRequest полна несоответствий в именах, с которыми нам придется сталкиваться:

Имя объекта XMLHttpRequest было выбрано исходя из соображений совместимости с Веб, хотя каждая часть этого имени может вводить в заблуждение. Во-первых, объект поддерживает любые текстовые форматы, включая XML. Во-вторых, он может использоваться для отправки запросов по обоим протоколам, HTTP и HTTPS (некоторые реализации могут поддерживать дополнительные протоколы, помимо HTTP и HTTPS, но эти возможности не регламентируются в спецификации). Наконец, он поддерживает «запросы» («requests») в более широком смысле, – как это подразумевает использование протокола HTTP, а именно, все операции, связанные с выполнением HTTP-запросов или получением HTTP-ответов для определенных HTTP-методов.

содержание 18.1. Использование объекта XMLHttpRequest содержание

Прикладной интерфейс к протоколу HTTP в браузерах определяется в виде класса XMLHttpRequest. Каждый экземпляр этого класса представляет единственную пару запрос/ответ, а свойства и методы объекта позволяют определять параметры запроса и извлекать данные из ответа. Объект XMLHttpRequest поддерживается веб- браузерами уже довольно давно, а его прикладной интерфейс находится на последних стадиях стандартизации консорциумом W3C. В то же время в консорциуме W3C ведутся работы над проектом стандарта «XMLHttpRequest Level 2». В этом разделе мы рассмотрим базовый прикладной интерфейс объекта XMLHttpRequest, а также те части проекта стандарта «XMLHttpRequest Level 2» (я называю его «XHR2»), которые в настоящее время реализованы как минимум в двух браузерах.

Первое, что обычно необходимо сделать при использовании этого прикладного интерфейса к протоколу HTTP, это, разумеется, создать экземпляр объекта XMLHttpRequest

var request = new XMLHttpRequest();

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

XMLHttpRequest в IE6

Корпорация включила поддержку объекта XMLHttpRequest в свой браузер IE, начиная с версии 5. В версиях IE5 и IE6 этот объект доступен только в виде ActiveX-объекта. Поддержка современного стандартного конструктора XML- HttpRequestQ появилась только в IE7, но его можно имитировать, как показано ниже:

// Имитация конструктора XMLHttpRequestO в IE5 и IE6
if (window.XMLHttpRequest === undefined) {
window.XMLHttpRequest = function() {
try {
// Использовать последнюю версию ActiveX-объекта, если доступна
return new ActiveX0bject("Msxml2.XMLHTTP.6.(T);
}
catch (e1) {
try {
// Иначе вернуться к старой версии
return new ActiveX0bject("Msxml2.XMLHTTP.3.0'');
}
catch(e2) {
// Если ничего не получилось - возбудить ошибку
throw new Error("XMLHttpRequest не поддерживается");
}
}
};
}

HTTP-запрос состоит из четырех частей:

Первые два подраздела, следующие далее, демонстрируют, как устанавливать каждую часть HTTP-запроса и как извлекать части из HTTP-ответа. За этими ключевыми разделами следуют подразделы, освещающие более узкоспециализированные темы.

Базовая архитектура запрос/ответ протокола HTTP весьма проста в использовании. Однако на практике возникает масса сложностей: клиенты и серверы обмениваются данными в виде cookies; серверы переадресуют браузеры на другие серверы; одни ресурсы кэшируются, а другие – нет; некоторые клиенты отправляют запросы через прокси-серверы и т.д. Объект XMLHttpRequest не является прикладным интерфейсом уровня протокола, он обеспечивает прикладной интерфейс уровня браузера. браузер сам заботится о cookies, переадресации, кэшировании и прокси-серверах, а вам достаточно позаботиться только о запросах и ответах.

XMLHttpRequest и локальные файлы

Возможность использования относительных URL-адресов в веб-страницах обычно означает, что HTML-страницы можно разрабатывать и проверять, используя локальную файловую систему, а затем перемещать их на веб-сервер без дополнительных изменений. Однако, как правило, это невозможно при использовании архитектуры Ajax на основе объекта XMLHttpRequest. Объект XMLHttpRequest предназначен для работы с протоколами HTTP и HTTPS. Теоретически он мог было работать с другими протоколами, такими как FTP, но такие части прикладного интерфейса, как метод запроса и код состояния ответа, являются характерными именно для протокола HTTP. Если загрузить веб-страницу из локального файла, сценарии в этой странице не смогут использовать объект XMLHttpRequest с относительными URL-адресами, потому что эти адреса будут относительными адресами вида file://, а не http://. А политика общего происхождения зачастую будет препятствовать использованию абсолютных адресов вида http://. (Тем не менее загляните в раздел 18.1.6.) Таким образом, чтобы проверить вебстраницы, использующие объект XMLHttpRequest, их необходимо выгружать на веб-сервер (или использовать локальный веб-сервер).

содержание18.1.1. Выполнение запроса

Следующий этап после создания объекта XMLHttpRequest – определение параметров HTTP-запроса вызовом метода ореn() объекта XMLHttpRequest, которому передаются две обязательные части запроса: метод и URL:

request.open("GET". // Запрос типа HTTP GET
"data.csv"); // на получение содержимого по этому URL-адресу

Первый аргумент метода open() определяет HTTP-метод или операцию. Это строка, не чувствительная к регистру, но обычно содержащая только символы верхнего регистра, в соответствии со спецификацией протокола HTTP. Методы «GET» и «POST» поддерживаются всеми браузерами. Метод «GET» используется для «обычных» запросов и соответствует случаю, когда URL-адрес полностью определяет запрашиваемый ресурс. Он используется, когда запрос не имеет побочных эффектов и когда ответ сервера можно поместить в кэш. Метод «POST» обычно используется HTML-формами. Он включает в тело запроса дополнительные данные (данные формы), и эти данные часто сохраняются в базе данных на стороне сервера (побочный эффект). В ответ на повторяющиеся запросы POST к одному и тому же URL сервер может возвращать разные ответы, и ответы на запросы, отправленные этим методом, не должны помещаться в кэш.

Помимо запросов «GET» и «POST», спецификация XMLHttpRequest также позволяет передавать методу open() строки «DELETE», «HEAD», «OPTIONS» и «PUT» в первом аргументе. (Методы «HTTP CONNECT», «TRACE» и «TRACK» явно запрещены к использованию из-за проблем с безопасностью.) Старые версии браузеров могут не поддерживать все эти методы, но метод «HEAD», по крайней мере, поддерживается почти всеми браузерами, и его использование демонстрируется в примере 18.13.

Вторым аргументом методу open() передается URL-адрес запрашиваемого ресурса. Это относительный URL-адрес документа, содержащего сценарий, в котором вызывается метод open(). Если указать абсолютный адрес, то в общем случае протокол, доменное имя и порт должны совпадать с аналогичными параметрами адреса документа: нарушение политики общего происхождения обычно вызывает ошибку. (Однако спецификация «XMLHttpRequest Level 2» допускает выполнение запросов к другим серверам, если сервер явно разрешил это – смотрите раздел 18.1.6 .)

Следующий этап в выполнении запроса – установка заголовков запроса, если это необходимо. Запросы POST, например, требуют, чтобы был определен заголовок «Content-Type», определяющий MIME-тип тела запроса:

request.setRequestHeader("Content-Type", "text/plain");

Если вызвать метод setRequestHeader() несколько раз с одним и тем же заголовком, новое значение не заменит прежнее: вместо этого в HTTP-запрос будет вставлено несколько копий заголовка или один заголовок с несколькими значениями.

Нельзя определять собственные заголовки «Content-Length», «Date», «Referer» и «User-Agent»: объект XMLHttpRequest добавляет их автоматически и не позволяет подделывать их. Аналогично объект XMLHttpRequest автоматически обрабатывает cookies и срок поддержки открытого соединения, определяет кодировку символов и выполняет кодирование сообщений, поэтому вы не должны передавать методу setRequestHeader() следующие заголовки:

Accept-CharsetContent-Transfer-Encoding TE
Accept-EncodingTrailer
ConnectionExpectTransfer-Encodin
Content-LengthHostUpgrade
CookieKeep-AliveUser-Agen
Cookie2RefererVia

В запросе можно определить заголовок «Authorization», но обычно в этом нет необходимости. При выполнении запроса к ресурсу, защищенному паролем, передайте имя пользователя и пароль методу open() в четвертом и пятом аргументах, а объект XMLHttpRequest автоматически установит соответствующие заголовки. (О третьем необязательном аргументе метода open() рассказывается ниже, а описание аргументов, в которых передаются имя пользователя и пароль, можно найти в справочной части книги.)

Последний этап в процедуре выполнения HTTP-запроса с помощью объекта XMLHttpRequest – передача необязательного тела запроса и отправка его серверу. Делается это с помощью метода send():

request.send(null);

GET-запросы не имеют тела, и в этом случае можно передать методу значение null или вообще опустить аргумент. POST-запросы обычно имеют тело, и оно должно соответствовать заголовку «Content-Type», установленному с помощью метода setRequestHeader().

Пример 18.1 демонстрирует использование всех методов объекта XMLHttpRequest, описанных выше. Он отправляет серверу текстовую строку методом POST и игнорирует ответ, возвращаемый сервером.

Пример 18.1. Отправка простого текста на сервер методом POST

function postMessage(msg) {
var request = new XMLHttpRequest(); // Новый запрос
request.open("POST", "/log.php"); // серверному сценарию методом POST
// Отправить простое текстовое сообщение в теле запроса
request.setRequestHeader("Content-Type", // Тело запроса - простой текст
"text/plain;charset=UTF-8");
request.send(msg); // msg как тело запроса
// Запрос выполнен. Мы игнорируем возможный ответ или ошибку.
}

Обратите внимание, что вызов метода send() в примере 18.1 инициирует запрос и затем возвращает управление: он не блокируется в ожидании ответа от сервера. HTTP-ответы практически всегда обрабатываются асинхронно, как будет показано в следующем разделе.

Порядок имеет значение

Части HTTP-запроса следуют в определенном порядке: метод запроса и URL-адрес должны определяться в первую очередь, затем должны устанавливаться заголовки запроса и, наконец, тело запроса. Обычно реализации XMLHttpRequest ничего не отправляют в сеть, пока не будет вызван метод send(). Но прикладной интерфейс XMLHttpRequest спроектирован так, как если бы каждый метод немедленно отправлял данные в сеть. Это означает, что методы объекта XMLHttpRequest должны вызываться в порядке, соответствующем структуре HTTP-запроса. Например, метод setRequestHeader() должен вызываться после метода open() и перед методом send(), в противном случае он возбудит исключение.

содержание18.1.2. Получение ответа

Полный HTTP-ответ содержит код состояния, набор заголовков ответа и тело ответа. Все это доступно в виде свойств и методов объекта XMLHttpRequest

Обычно объект XMLHttpRequest используется в асинхронном режиме (но загляните в раздел 18.1.2.1): метод send() возвращает управление сразу же после отправки запроса, поэтому методы и свойства, перечисленные выше, не могут использоваться до фактического получения ответа. Чтобы определить момент получения ответа, необходимо обрабатывать событие «readystatechange» (или событие «progress», определяемое новой спецификацией «XHR2» и описываемое в разделе 18.1.4), возбуждаемое в объекте XMLHttpRequest. Но, чтобы понять, как обрабатывать это событие, необходимо сначала разобраться со свойством readyState.

Свойство readyState – это целочисленное значение, определяющее код состояния HTTP-запроса; его возможные значения перечислены в табл. 18.1. Идентификаторы, указанные в первой колонке, – это константы, определяемые конструктором XMLHttpRequest. Эти константы являются частью спецификации XMLHttpRequest, но старые версии браузеров и IE8 не определяют их, поэтому часто можно увидеть программный код, в котором вместо константы XMLHttpRequest. DONE используется числовое значение 4.

Теоретически событие «readystatechange» генерируется всякий раз, когда изменяется значение свойства readyState. На практике же событие может не возбуждаться, когда свойство readyState получает значение 0 или 1. Оно часто возбуждается при вызове метода send(), даже при том, что свойство readyState по-прежнему содержит значение OPENED. Некоторые браузеры возбуждают событие множество раз для состояния LOADING, чтобы обеспечить обратную связь. Все браузеры возбуждают событие «readystatechange», когда завершается прием ответа сервера и свойство readyState получает значение 4. Так как это событие может возбуждаться еще дозавершения приемаответа, обработчики события «readystatechange» всегда должны проверять значение свойства readyState.

Чтобы обрабатывать события «readystatechange», нужно присвоить функцию обработчика события свойству onreadystatechange объекта XMLHttpRequest. Можно также воспользоваться методом addEventListener() (или attachEvent() в IE версии 8 и ниже), но обычно для обработки запроса бывает вполне достаточно одного обработчика, поэтому проще установить свойство onreadystatechange.

Таблица 18.1. Значения свойства readyState объекта XMLHttpRequest

КонстантаЗначение Смысл
UNSENT0Метод open() еще не был вызван
OPENED1Метод open() был вызван
HEADERS_RECEIVED2Были получены заголовки
LOADING3Идет прием тела ответа
DONE4Прием ответа завершен

Пример 18.2 определяет функцию getText(), которая демонстрирует особенности обработки событий «readystatechange». Обработчик события сначала проверяет завершение запроса. После этого он проверяет код состояния ответа и убеждается в успешном выполнении. Затем он извлекает заголовок «Content-Type», чтобы убедиться, что получен ответ ожидаемого типа. Если выполняются все три условия, он передает тело ответа (в виде текста) указанной функции обратного вызова.

Пример 18.2. Получение HTTP-ответа в обработчике onreadystatechange

Пример 18.2. Получение HTTP-ответа в обработчике onreadystatechange

// Выполняет запрос HTTP GET содержимого указанного URL-адреса.
// После успешного получения ответа проверяет, содержит ли он простой текст, 
// и передает его указанной функции обратного вызова 
function getText(url, callback) {
  var request = new XMLHttpRequest();	// Создать новый запрос
  request.open( "GET", url);	// Указать URL-адрес ресурса
  request.onreadystatechange = function() {	// Определить обработчик события
    // Если запрос был выполнен успешно
    if (request.readyState === 4 && request.status === 200) {
      var type = request.getResponseHeader("Content-Type");
      if (!type.match(/^text/))	// Убедиться, что это текст
        callback(request.responseText);;	// Передать функции
    }
  }
  request.send(null);	// Отправить запрос
}

содержание18.1.2.1. Получение синхронного ответа

Сама природа HTTP-ответа предполагает их асинхронную обработку. Тем не менее объект XMLHttpRequest поддерживает возможность получения ответов в синхронном режиме. Если в третьем аргументе передать методу open() значение false, выполнение метода send() будет заблокировано до завершения запроса. В этом случае отпадает необходимость использовать обработчик события: после того как метод send() вернет управление, можно будет сразу же проверить свойства status и responseText объекта XMLHttpRequest. Сравните следующую синхронную реализацию функции getText() из примера 18.2:

// Выполняет синхронный запрос HTTP GET содержимого по указанному URL-адресу.
// Возвращает текст ответа. Возбуждает исключение в случае неудачи 
// или если ответ не является текстом, 
// Выполняет синхронный запрос HTTP GET содержимого по указанному URL-адресу.
// Возвращает текст ответа. Возбуждает исключение в случае неудачи 
// или если ответ не является текстом, 
function getTextSync(url) {
  var request = new XMLHttpRequest() // Создать новый запрос
  request.open( "GET", url, false);  // false - синхронный режим
  request.send(null);                // Отправить запрос

  // Возбудить исключение, если код состояния не равен 200
  if (request.status !== 200) throw new Error(request.statusText)

  // Возбудить исключение, если ответ имеет недопустимый тип
  var type = request.getResponseHeader("Content-Type");
  if (!type.match(/^text/))
  throw new Error("Ожидался текстовый ответ; получен: " + type);

  return request.responseText;
}

Синхронные запросы выглядят весьма заманчиво, однако использовать их нежелательно. Интерпретатор JavaScript на стороне клиента выполняется в единственном потоке, и когда метод send() блокируется, это обычно приводит к зависанию пользовательского интерфейса всего браузера. Если сервер, к которому выполнено подключение, отвечает на вопросы с задержкой, браузер пользователя будет зависать. Тем не менее в разделе 22.4 вы познакомитесь с одним из случаев, когда синхронные запросы вполне допустимы.

содержание18.1.2.2. Декодирование ответа

В приведенных выше примерах предполагалось, что сервер возвращает ответ в виде простого текста, с MIME-типом «text/plain», «text/html» или «text/css», и мы извлекаем его из свойства responseText объекта XMLHttpRequest.

Однако существуют и другие способы обработки ответов сервера. Если сервер посылает в ответе XML- или XHTML-документ, разобранное представление XML-документа можно получить из свойства responseXML. Значением этого свойства является объект Document, при работе с которым можно использовать приемы, представленные в главе 15 . (Проект спецификации «XHR2» требует, чтобы браузеры автоматически выполняли синтаксический анализ ответов типа «text/html» и также делали их доступными через свойство responseXML в виде объектов Document, но на момент написания этих строк это требование не было реализовано в текущих браузерах.)

Если в ответ на запрос серверу потребуется отправить структурированные данные, такие как объект или массив, он может передать данные в виде строки в формате JSON (раздел 6.9). После получения такой строки содержимое свойства responseText можно передать методу JSON.parse(). Пример 18.3 является обобщенной версией примера 18.2: он выполняет запрос методом GET по указанному URL-адресу и после получения содержимого этого адреса передает его указанной функции обратного вызова. Но теперь функции не всегда будет передаваться простой текст – ей может быть передан объект Document, объект, полученный с помощью JSON.parse(), или строка.

Пример 18.3. Синтаксический анализ НТТР-ответа

// Выполняет запрос HTTP GET на получение содержимого по указанному URL-адресу.
// При получении ответа он передается функции обратного вызова 
// как разобранный объект XML-документа, объект JS0N или строка, 
function get(url, callback) {
  var request = new XMLHttpRequest();                 // Создать новый запрос
  request.open("GET", url); // Указать URL-адрес ресурса
  request.onreadystatechange = function() {           // Определить обработчик события
    // Если запрос был выполнен и увенчался успехом
    if (request.readyState === 4 && request.status === 200) {
      // Определить тип ответа
      var type = request.getResponseHeader("Content-Type");
      // Проверить тип, чтобы избежать в будущем передачи ответа
      // в виде документа в формате HTML
      if (type.index0f("xml") !== -1 && request.responseXML)
        callback(request.responseXML);                // Объект XML
      else
        if (type === "application/json")
          callback(JSON.parse(request.responseText)); // Объект JSON
        else
          callback(request.responseText);             // Строка
    }
  };
  request.send(null); // Отправить запрос
}

Функция в примере 18.3 проверяет заголовок «Content-Type» ответа и обрабатывает ответы типа «application/json» особым образом. Другими типами ответов, которые может потребоваться «декодировать» особо, являются «application/Java-Script» и «text/JavaScript». С помощью объекта XMLHttpRequest можно запрашивать сценарии на языке JavaScript и затем выполнять их с помощью глобальной функции eval() (раздел 4.12.2). Однако в этом нет никакой необходимости, потому что возможностей самого элемента <script> вполне достаточно, чтобы загрузить и выполнить сценарий. Вернитесь к примеру 13.4, держа в уме, что элемент <script> может выполнять HTTP-запросы к другим серверам, запрещенные в прикладном интерфейсе XMLHttpRequest.

В ответ на HTTP-запросы веб-серверы часто возвращают двоичные данные (например, файлы изображений). Свойство responseText предназначено только для текстовых данных и не позволяет корректно обрабатывать ответы с двоичными данными, даже если использовать метод charCodeAt() полученной строки. Спецификация «ХНН2» определяет способ обработки ответов с двоичными данными, но на момент написания этих строк производители браузеров еще не реализовали его. Дополнительные подробности приводятся в разделе 22.6.2

Для корректного декодирования ответа сервера необходимо, чтобы сервер отправлял заголовок «Content-Type» с правильным значением MIME-типа ответа. Если, к примеру, сервер отправит XML-документ, не указав соответствующий MIME-тип, объект XMLHttpRequest не произведет синтаксический анализ ответа и не установит значение свойства responseXML. Или, если сервер укажет неправильное значение в параметре «charset» заголовка «Content-Type», объект XMLHttpRequest декодирует ответ с использованием неправильной кодировки и в свойстве responseText могут оказаться ошибочные символы.

Спецификация «ХНН2» определяет метод overrideMimeType(), предназначенный для решения этой проблемы, и он уже реализован в некоторых браузерах. Если необходимо определить MIME-тип, лучше подходящий для веб-приложения, чем возвращаемый сервером, можно перед вызовом метода send() передать методу overrideMimeType() свой тип – это заставит объект XMLHttpRequest проигнорировать заголовок «Content-Type» и использовать указанный тип. Предположим, что необходимо загрузить XML-файл, который планируется интерпретировать как простой текст. В этом случае можно воспользоваться методом overrideMimeType(), чтобы сообщить объекту XMLHttpRequest, что он не должен выполнять синтаксический анализ файла и преобразовывать его в объект XML-документа:

// Не обрабатывать ответ, как XML-документ
request.overrideMimeType("text/plain; charset=utf-8")

содержание18.1.3. Оформление тела запроса

Запросы HTTP POST включают тело запроса, которое содержит данные, передаваемые клиентом серверу. В примере 18.1 тело запроса было простой текстовой строкой. Однако нередко бывает необходимо передать в HTTP-запросе более сложные данные. В этом разделе демонстрируются некоторые способы реализации отправки таких данных.

содержание18.1.3.1. Запросы с данными в формате HTML-фор

Рассмотрим HTML-формы. Когда пользователь отправляет форму, данные в форме (имена и значения всех элементов формы) помещаются в строку и отправляются вместе с запросом. По умолчанию HTML-формы отправляются на сервер методом POST, и данные формы помещаются в тело запроса. Схема преобразования данных формы в строку относительно проста: к имени и значению каждого элемента формы применяется обычное URI-кодирование (замена специальных символов шестнадцатеричными кодами), кодированные представления имен и значений отделяются знаком равенства, а пары имя/значение - амперсандом. Представление простой формы в виде строки может выглядеть, как показано ниже:

find=pizza&zipcode=02134&radius=1km

Такой формат представления данных формы соответствует формальному MIME-типу:

application/x-www-form-urlencoded>

Этот тип следует указать в заголовке «Content-Type» запроса при отправке данных такого вида в составе запроса методом POST.

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

{
find: "pizza",
zipcode: 02134,
radius: "1km"
}

Формат представления данных форм настолько широко используется во Всемирной паутине и настолько хорошо поддерживается всеми языками программирования для создания серверных сценариев, что его применение для представления данных, никак не связанных с формами, часто оказывается наиболее простым и удобным. Пример 18.4 демонстрирует, как преобразовать свойства объекта в формат представления формы.

Пример 18.4. Преобразование объекта в формат для отправки в НТТР-запросе

/**
* Представляет свойства объекта, как если бы они были парами имя/значение * HTML-формы, с использованием формата application/x-www-form-urlencoded */ function encodeFormData(data) { if (!data) return // Всегда возвращать строку var pairs = []; // Для пар имя/значение for(var name in data) { // Для каждого имени if (!data.hasOwnProperty(name)) continue; // Пропустить унаслед. if (typeof data[name] === "function") continue;// Пропустить методы var value = data[name].toString(); // Знач, в виде строки name = encodeURIComponent(name.replace(”%20”, "+”)); // Кодировать имя value = encodeURIComponent(value.replace("%20". "+")); // Кодировать значение pairs.push(name + "=” + value); // Сохранить пару имя/значение } return pairs.join('&’); // Объединить пары знаком & и вернуть }

С помощью этой функции encodeFormData() легко можно создавать утилиты, такие как функция postData(), представленная в примере 18.5. Обратите внимание, что для простоты эта функция postData() (и аналогичные ей функции в примерах ниже) не обрабатывает ответ сервера. При получении ответа она передает объект XMLHttpRequest целиком указанной функции обратного вызова. Эта функция обратного вызова сама должна проверять код состояния ответа и извлекать содержимое ответа.

Пример 18.5. Выполнение запроса HTTP POST с данными в формате представления форм

function postData(url, data, callback) {
  var request = new XMLHttpRequest();
  request.open("POST", url); // Методом POST на указ, url
  request.onreadystatechange = function() { // Простой обработчик
    if (request.readyState === 4 && callback) // При получении ответа
      callback(request); // вызвать указанную функцию
  };
  request.setRequestHeader("Content-Type", // Установить "Content-Type"
      "application/x-www-form-urlencoded");
  request.send(encodeFormData(data)); // Отправить данные
} // в представлении форм

Данные формы также могут быть отправлены посредством GET-запроса, и когда цель формы состоит в том, чтобы определить параметры операции чтения, метод GET лучше соответствует назначению формы, чем метод POST. GET-запросы не имеют тела, поэтому «полезный груз» с данными формы отправляется серверу в виде строки запроса в адресе URL (следующей за знаком вопроса). Утилита encodeFormData() может также пригодиться для отправки подобных GET-запросов, и пример 18.6 демонстрирует такое ее применение.

Пример 18.6. Выполнение GET-запроса с данными в формате представления форм

function getData(url, data, callback) {
  var request = new XMLHttpRequest();
  request.open("GET", url + // Методом GET на указанный url
      "?" + encodeFormData(data)); // с добавлением данных
  request.onreadystatechange = function() { // Простой обработчик событий
    if (request.readyState === 4 && callback) callback(request);
  };
  request.send(null); // Отправить запрос
}

Для добавления данных в URL-адреса HTML-формы используют строки запросов, но использование объекта XMLHttpRequest дает свободу представления данных.

содержание18.1.3.2. Запросы с данными в формате JSON

Использование формата представления данных форм в теле POST-запросов является распространенным соглашением, но не является обязательным требованием протокола HTTP. В последние годы в роли формата обмена данными во Всемирной паутине все большей популярностью пользуется формат JSON. Пример 18.7 демонстрирует, как с помощью функции JSON.stringifу() (раздел 6.9) можно сформировать тело запроса. Обратите внимание, что этот пример отличается от примера 18.5 только последними двумя строками.

Пример 18.7. Выполнение запроса HTTP POST с данными в формате JSON

function postJS0N(url, data, callback) {
  var request = new XMLHttpRequest():
  request.open("POST”, url); // Методом POST на указ, url
  request.onreadystatechange = function() { // Простой обработчик
    if (request.readyState === 4 && callback) // При получении ответа
      callback(request): // вызвать указанную функцию
  };
  request.setRequestHeader("Content-Type", "application/json");
  request.send(JS0N.stringify(data));

содержание18.1.3.3. Запросы с данными в формате XML

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

<query>
<find zipcode="02134" radius="1km"> pizza </find>
</query>

Во всех примерах, встречавшихся до сих пор, аргументом метода send() объекта XMLHttpRequest была строка или значение null. В действительности же этому методу можно также передать объект Document XML-документа. Пример 18.8 демонстрирует, как создать объект Document простого XML-документа и использовать его в качестве тела НТТР-запроса.

Пример 18.8. Выполнение запроса HTTP POST с XML-документом в качестве тела

// Параметры поиска "что", "где" и "радиус" оформляются в виде XML-документа // и отправляются по указанному URL-адресу. При получении ответа вызывает // указанную функцию
function post()uery(url, what, where, radius, callback) {
  var request = new XMLHttpRequest();
  request.open("POST", url); // Методом POST на указанный url
  request.onreadystatechange = function() { // Простой обработчик
    if (request.readyState === 4 && callback) callback(request);
  };
  // Создать XML-документ с корневым элементом <query>
  var doc = document.implementation.createDocument("", "query", null);
  var query = doc.documentElement; // Элемент <query>
  var find = doc.createElement("find"); // Создать элемент <find>
  query.appendChild(find); // И добавить в <query>
  find.setAttribute("zipcode", where); // Атрибуты <find>
  find.setAttribute("radius”, radius);
  find.appendChild(doc.createTextNode(what)); // И содержимое <find>
  // Отправить данные в формате XML серверу.
  // Обратите внимание, что заголовок Content-Type будет установлен автоматически, 
  request.send(doc);
}

Обратите внимание, что пример 18.8 не устанавливает заголовок «Content-Type» запроса. Когда методу send() передается XML-документ, то объект XMLHttpRequest автоматически установит соответствующий заголовок «Content-Type», если он не был установлен предварительно. Аналогично, если передать методу send() простую строку и не установить заголовок «Content-Type», объект XMLHttpRequest автоматически добавит этот заголовок со значением «text/plain; charset=UTF-8». Программный код в примере 18.1 явно устанавливает этот заголовок, но в действительности для данных в простом текстовом виде этого не требуется.

содержание18.1.3.4. Выгрузка файлов

Одна из особенностей HTML-форм заключается в том, что, если пользователь выберет файл с помощью элемента <input type="file">, форма отправит содержимое этого файла в теле POST-запроса. HTML-формы всегда позволяли выгружать файлы, но до недавнего времени эта операция была недоступна в прикладном интерфейсе XMLHttpRequest. Прикладной интерфейс, определяемый спецификацией «ХНН2», позволяет выгружать файлы за счет передачи объекта File методу send().

В данном случае нельзя создать объект с помощью конструктора File(): сценарий может лишь получить объект File, представляющий файл, выбранный пользователем. В браузерах, поддерживающих объекты File, каждый элемент <input type="file"> имеет свойство files, которое ссылается на объект, подобный массиву, хранящий объекты File. Прикладной интерфейс буксировки (drag-and-drop) (раздел 17.7) также позволяет получить доступ к файлам, «сбрасываемым» пользователем на элемент, через свойство dataTransfer.files события «drop». Поближе с объектом File мы познакомимся в разделах 22.6 и 22.7. А пока будем рассматривать объект File как полностью непрозрачное представление выбранного пользователем файла, пригодное для выгрузки методом send(). В примере 18.9 представлена ненавязчивая JavaScript-функция, добавляющая обработчик события «change» к указанным элементам выгрузки файлов, чтобы они автоматически отправляли содержимое любого выбранного файла методом POST на указанный адрес URL.

Пример 18.9. Выгрузка файла посредством запроса HTTP POST

// Отыскивает все элементы <input type=”file"> с атрибутом data-uploadto
// и регистрирует обработчик onchange, который автоматически отправляет 
// выбранный файл по указанному URL-адресу "uploadto". Ответ сервера игнорируется. 
whenReady(function() { // Вызвать эту функцию после загрузки документа
  var elts = document.getElementsByTagName("input”); // Все элементы input
  for(var і = 0; і < elts.length; i++) { // Обойти в цикле
    var input = elts[i];
    if (input.type !== ’'file") continue; // Пропустить все, кроме
                                          // элементов выгрузки файлов
    var url = input.getAttribute("data-uploadto"); // Адрес выгрузки
    if (!url) continue; // Пропустить элементы без url
    input.addEventListener("change", function() { // При выборе файла
      var file = this.files[0]; // Предполагается выбор единственного файла
      if (!file) return; // Если файл не выбран, ничего не делать
      var xhr = new XMLHttpRequest(); // Создать новый запрос
      xhr.open("POST", url); // Методом POST на указанный URL
      xhr.send(file); // Отправить файл в теле запроса
    }, false);
  }
});

Как будет показано в разделе 22.6, тип File является подтипом более общего типа Blob. Спецификация «ХНН2» позволяет передавать методу send() произвольные объекты Blob. Свойство type объекта Blob в этом случае будет использоваться для установки заголовка «Content-Type», если он не будет определен явно. Если потребуется выгрузить двоичные данные, сгенерированные клиентским сценарием, можно воспользоваться приемами преобразования данных в объект Blob, демонстрируемыми в разделах 22.5 и 22.6.3, и передавать в виде тела запроса этот объект.

содержание18.1.3.5. Запросы с данными в формате multipart/form-data

Когда наряду с другими элементами HTML-формы включают элементы выгрузки файлов, браузер не может использовать обычный способ представления данных форм и должен отправлять формы, используя специальное значение «multipart/form-data» в заголовке «Content-Type». Этот формат связан с использованием длинных «граничных» строк, делящих тело запроса на несколько частей. Для текстовых данных можно вручную создать тело «multipart/form-data» запроса, но это довольно сложно.

Спецификация «ХНН2» определяет новый прикладной интерфейс FormData, упрощающий создание тела запроса, состоящего из нескольких частей. Сначала с помощью конструктора FormData() создается объект FormData, а затем вызовом метода append() этого объекта в него добавляются отдельные «части» (которые могут быть строками или объектами File и Blob). В заключение объект FormData передается методу send(). Метод send() определит соответствующую строку, обозначающую границу, и установит заголовок «Content-Type» запроса. Пример 18.10 демонстрирует использование объекта FormData, с которым мы еще встретимся в примере 18.11.

Пример 18.10. Отправка запроса с данными в формате multipart/form-data

function postFormData(url, data, callback) {
  if (typeof FormData === "undefined")
    throw new Error("Объект FormData не реализован");
  var request = new XMLHttpRequest(); // Новый HTTP-запрос
  request.open("POST", url); // Методом POST на указанный URL
  request.onreadystatechange = function() { // Простой обработчик,
    if (request.readyState === 4 && callback) // При получении ответа
      callback(request); // вызвать указанную функц.
  };
  var formdata = new FormData();
  for(var name in data) {
    if (!data.hasOwnProperty(name)) continue; // Пропустить унасл. св-ва
    var value = data[name];
    if (typeof value === "function”) continue; // Пропустить методы
    // Каждое свойство станет отдельной "частью" тела запроса.
    // Допускается использовать объекты File
    formdata.append(name, value); // Добавить имя/значение,
  } // как одну часть
  // Отправить пары имя/значение в теле запроса multipart/form-data. Каждая пара -
  // это одна часть тела запроса. Обратите внимание, что метод send автоматически
  // устанавливает заголовок Content-Type, когда ему передается объект FormData
  request.send(formdata);
}

содержание18.1.4. События, возникающие в ходе выполнения HTTP-запроса

В примерах выше для определения момента завершения HTTP-запроса использовалось событие «readystatechange». Проект спецификации «XHR2» определяет более удобный набор событий, уже реализованный в Firefox, Chrome и Safari. В этой новой модели событий объект XMLHttpRequest генерирует различные типы событий на разных этапах выполнения запроса, благодаря чему отпадает необходимость проверять значение свойства readyState.

Далее описывается, как генерируются эти новые события в браузерах, поддерживающих их. Когда вызывается метод sendQ, один раз возбуждается событие «load- start». В ходе загрузки ответа сервера объект XMLHttpRequest возбуждает серию событий «progress», обычно каждые 50 миллисекунд или около того, которые можно использовать для обратной связи с пользователем, чтобы информировать его о ходе выполнения запроса. Если запрос завершается очень быстро, событие «progress» может и не возбуждаться. По завершении запроса возбуждается событие «load».

Завершение запроса не всегда означает успешное его выполнение, поэтому обработчик события «load» должен проверять код состояния в объекте XMLHttpRequest, чтобы убедиться, что был принят НТТР-код «200 ОК», а не «404 Not Found», например.

Существуют три разные ситуации, когда HTTP-запрос оканчивается неудачей, которым соответствуют три события. Если предельное время ожидания ответа истекло, генерируется событие «timeout». Если выполнение запроса было прервано, генерируется событие «abort». (О предельном времени ожидания и о методе abort() подробнее рассказывается в 18.1.5.) Наконец, выполнению запроса могут препятствовать другие ошибки в сети, такие как слишком большое количество переадресаций, и в этих случаях генерируется событие «error».

Для каждого запроса браузер может возбуждать только по одному событию «load», «abort», «timeout» и «error». Проект спецификации «XHR2» требует, чтобы браузеры возбуждали событие «loadend» после одного из этих событий. Однако на момент написания этих строк событие «loadend» не было реализовано ни в одном из браузеров.

Для регистрации обработчиков всех этих событий, возникающих в ходе выполнения запроса, можно использовать метод addEventListener() объекта XMLHttpRequest. Если каждое из этих событий обрабатывается единственным обработчиком, эти обработчики обычно проще установить, присвоив их соответствующим свойствам объекта, таким как onprogress и onload. Определяя наличие этих свойств, можно даже проверить поддержку соответствующих событий в браузере:

if ("onprogress" in (new XMLHttpRequest())) {
// События, возникающие в ходе выполнения запроса, поддерживаются
}

Объект события, связанный с этими событиями, возникающими в ходе выполнения запроса, в дополнение к свойствам обычного объекта Event, таким как type и timestamp, добавляет три полезных свойства. Свойство loaded определяет количество байтов, переданных к моменту возбуждения события. Свойство total содержит общий объем (в байтах) загружаемых данных, определяемый из заголовка «Content-Length», или 0, если объем содержимого не известен. Наконец, свойство lengthComputable содержит значение true, если общий объем содержимого известен, и false - в противном случае. Очевидно, что свойства total и loaded особенно полезны в обработчиках событий, возникающих в ходе выполнения запроса:

request.onprogress = function(e) { 
if (e.lengthComputable)
progress.innerHTML = Math.round(100*e.loaded/e.total) + "% Выполнено";
}

содержание18.1.4.1. События, возникающие в ходе выгрузки

Кроме событий, которые удобно использовать для мониторинга загрузки НТТР-ответа, спецификация «ХНН2» также определяет события для мониторинга выгрузки HTTP-запроса. В браузерах, реализующих эту возможность, объект XMLHttpRequest имеет свойство upload. Значением свойства upload является объект, определяющий метод addEventListener() и полный набор свойств-событий хода выполнения операции выгрузки, таких как onprogress и onload. (Однако этот объект не определяет свойство onreadystatechange: в процессе выгрузки генерируются только эти новые события.)

Обработчики событий хода выполнения операции выгрузки можно использовать точно так же, как используются обычные обработчики событий хода выполнения загрузки. Для объекта х типа XMLHttpRequest обработчик х.onprogress позволяет вести мониторинг хода выполнения загрузки ответа. А свойство х.upload.onprogress – мониторинг хода выполнения выгрузки запроса.

Пример 18.11 демонстрирует, как использовать событие «progress», генерируемое в ходе выгрузки запроса, для организации обратной связи с пользователем. Этот пример также демонстрирует, как получать объекты File с применением механизма буксировки (drag-and-drop) и как с помощью объекта FormData выгружать сразу несколько файлов в одном запросе XMLHttpRequest. На момент написания этих строк спецификации, определяющие эти особенности, находились в состоянии проектирования и этот пример работал не во всех браузерах.

Пример 18.11. Мониторинг хода выполнения операции выгрузки

// Отыскивает все элементы с классом "fileDropTarget" и регистрирует 
// обработчики событий механизма DnD, чтобы они могли откликаться 
// на операции буксировки файлов. При сбросе файлов на эти элементы 
// они выгружают их на URL-адрес, указанный в атрибуте data-uploadto. 
whenReady(function() {
  var elts = document.getElementsByClassName("fileDropTarget");
  for(var і = 0; і < elts.length; i++) {
    var target = elts[i];
    var url = target.getAttribute("data-uploadto");
    if (!url) continue;
    createFileUploadDropTarget(target, url);
  }
  function createFileUploadDropTarget(target, url) {
    // Следит за ходом выполнения операции выгрузки и позволяет отвергнуть 
    // выгрузку файла. Можно было бы обрабатывать сразу несколько параллельных 
    // операций выгрузки, но это значительно усложнило бы 
    // отображение хода их выполнения,
    var uploading = false;
    console.log(target.url);
    target.ondragenter = function(e) {
      console. log("dragenter");
      if (uploading) return; // Игнорировать попытку сброса, если
      // элемент уже занят выгрузкой файла
      var types = e.dataTransfer.types;
      if (types &&
          ((types.contains && types.contains("Files")) ||
          (types.indexOf && types.index0f("Files”) !== -1))) {
        target.classList.add("wantdrop");
        return false;
      }
    };
    target.ondragover = function(e) {
      if (!uploading) return false;
    };
    target.ondragleave = function(e) {
      if (!uploading) target.classList.remove("wantdrop");
    };
    target.ondrop = function(e) {
      if (uploading) return false;
      var files = e.dataTransfer.files;
      if (files && files.length) {
        uploading = true;
        var message = "Выгружаются файлы:<ul>";
        for(var і = 0; і < files.length; i++)
          message += "<li>" + files[i].name + "</li>";
        message += "</ul>";
        target.innerHTML = message;
        target.classList.remove("wantdrop");
        target.classList.add("uploading");
        var xhr = new XMLHttpRequest();
        xhr.open("POST", url);
        var body = new FormData();
        for(var i=0; і < files.length; i++)
          body.append(i, files[і]);
        xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
            target.innerHTML = message +
            Math.round(e.loaded/e.total*100) +
                             "% Завершено";
          }
        };
        xhr.upload.onload = function(e) {
          uploading = false;
          target.classList.remove("uploading");
          target.innerHTML = "Отбуксируйте сюда файл для выгрузки";
        };
        xhr.send(body);
        return false;
      }
      target.classList. removefwantdrop");
    }
  }
});

содержание18.1.5. Прерывание запросов и предельное время ожидания

Выполнение HTTP-запроса можно прерывать вызовом метода abort() объекта XMLHttpRequest. Метод abort() доступен во всех версиях объекта XMLHttpRequest, и согласно спецификации «XHR2» вызов метода abort() генерирует событие «abort». (На момент написания этих строк некоторые браузеры уже поддерживали событие «abort». Наличие этой поддержки можно определить по присутствию свойства onabort в объекте XMLHttpRequest.)

Основная причина для вызова метода abort() - появление необходимости отменить запрос, превышение предельного времени ожидания или если ответ становится ненужным. Допустим, что объект XMLHttpRequest используется для запроса подсказки в механизме автодополнения для текстового поля ввода. Если пользователь успеет ввести в поле новый символ еще до того, как подсказка будет получена с сервера, надобность в этой подсказке отпадает и запрос можно прервать.

Спецификация «XHR2» определяет свойство timeout, в котором указывается промежуток времени в миллисекундах, после которого запрос автоматически будет прерван, а также определяет событие «timeout», которое должно генерироваться (вместо события «abort») по истечении установленного промежутка времени. На момент написания этих строк браузеры еще не поддерживали автоматическое прерывание запроса по истечении предельного времени ожидания (и объекты XMLHttpRequest в них не имели свойств timeout и ontimeout). Однако имеется возможность реализовать собственную поддержку прерывания запросов по истечении заданного интервала времени с помощью функции setTimeout() (раздел 14.1) и метода abort(). Как это сделать, демонстрирует пример 18.12.

Пример 18.12. Реализация поддержки предельного времени ожидания

// Выполняет запрос HTTP GET на получение содержимого указанного URL.
// В случае благополучного получения ответа передает содержимое responseText функции
// обратного вызова. Если ответ не пришел в течение указанного времени, выполнение 
// запроса прерывается. браузеры могут возбуждать событие "readystatechange" после 
// вызова abort(), а в случае получения части ответа свойство status может даже 
// свидетельствовать об успехе, поэтому необходимо установить флаг, чтобы избежать 
// вызова функции в случае получения части ответа при прерывании запроса по превышению 
// времени ожидания. Эта проблема не наблюдается при использовании события load, 
function timedGetText(url, timeout, callback) {
  var request = new XMLHttpRequest(); // Создать новый запрос,
  var timedout = false; // Истекло ли время ожидания.
  // Запустить таймер, который прервет запрос по истечении
  // времени, заданного в миллесекундах.
  var timer = setTimeout(function() { // Запустить таймер. Если сработает,
      timedout = true; // установить флаг
      request.abort(); // и прервать запрос.
  };
  timeout); // Интервал времени ожидания
  request.open( "GET", url); // Указать URL запроса
  request.onreadystatechange = function() { // Обработчик события.
    if (request.readyState !== 4) return; // Игнорировать незаконч. запрос
    if (timedout) return; // Игнорировать прерв. запрос
    clearTimeout(timer); // Остановить таймер,
    if (request.status === 200) // В случае успеха
      callback(request.responseText); // передать ответ функции.
  };
  request.send(null); // Отправить запрос
}

содержание18.1.6. Выполнение междоменных HTTP-запросо

Будучи субъектом политики общего происхождения (раздел 13.6.2), объект XMLHttpRequest может использоваться для отправки HTTP-запросов только серверу, откуда был получен использующий его документ. Это ограничение закрывает дыру безопасности, но также предотвращает возможность использования объекта для вполне законных междоменных запросов. Элементы <form> и <iframe> позволяют указывать URL-адреса с другими доменными именами, и в этих случаях браузеры будут отображать документы, полученные из других доменов. Но из-за ограничений политики общего происхождения браузер не позволит оригинальному сценарию исследовать содержимое стороннего документа. При использовании объекта XMLHttpRequest содержимое документа всегда доступно через свойство responseText, поэтому политика общего происхождения не позволяет объекту XMLHttpRequest выполнять междоменные запросы. (Обратите внимание, что элемент <script> в действительности никогда не был субъектом политики общего происхождения: он будет загружать и выполнять любые сценарии, независимо от их происхождения. Как будет показано в разделе 18.2, такая свобода выполнения междоменных запросов делает элемент <script> привлекательной альтернативой Ajax-транспорту на основе объекта XMLHttpRequest.)

Спецификация «ХНН2» позволяет выполнять междоменные запросы к другим веб-сайтам, указанным в заголовке «CORS» (Cross-Origin Resource Sharing) HTTP-ответа. На момент написания этих строк текущие версии браузеров Firefox, Safari и Chrome поддерживали заголовок «CORS», а версия IE8 поддерживала собственный объект XDomainRequest, который здесь не описывается. Веб-программисту при этом не придется делать ничего особенного: если браузер поддерживает заголовок «CORS» для объекта XMLHttpRequest и если вы пытаетесь выполнить междоменный запрос к веб-сайту, доступ к которому разрешен в заголовке « CORS», политика общего происхождения просто не будет препятствовать выполнению междоменного запроса, и все будет работать само собой.

Хотя при наличии поддержки заголовка «CORS» не требуется предпринимать какие-то дополнительные шаги для выполнения междоменных запросов, тем не менее имеются некоторые детали, касающиеся безопасности, которые желательно понимать. Во-первых, при передаче имени пользователя и пароля методу open() объекта XMLHttpRequest они не будут передаваться в междоменных запросах (в противном случае это дало бы злоумышленникам возможность взламывать пароли). Кроме того, междоменные запросы обычно не допускают включение в них никакой другой информации, подтверждающей подлинность пользователя: cookies и HTTP-лексемы аутентификации обычно не передаются в составе запроса, а любые cookies, полученные в результате выполнения междоменного запроса, уничтожаются. Если для выполнения междоменного запроса требуется выполнить аутентификацию пользователя, перед вызовом метода send() следует установить свойство withCredentials объекта XMLHttpRequest в значение true. Проверка наличия свойства withCredentials позволит определить наличие поддержки заголовка «CORS» в браузере, хотя обычно этого не требуется.

В примере 18.13 приводится ненавязчивый JavaScript-код, использующий объект XMLHttpRequest для выполнения HEAD-запросов HTTP с целью получения информации о типах, размерах и датах последнего изменения ресурсов, на которые ссылаются элементы <а>. HEAD-запросы выполняются по требованию, а полученная информация отображается во всплывающих подсказках. В этом примере предполагается, что информация не будет доступна для междоменных ссылок, тем не менее в браузерах с поддержкой заголовка «CORS» все равно будут выполняться попытки получить ее.

Пример 18.13. Получение информации о ссылках с помощью HEAD-запросов при наличии поддержки заголовка «CORS»

/**
* linkdetails.js
*
* Этот модуль в стиле ненавязчивого JavaScript отыскивает все элементы <а>
* с атрибутом href и без атрибута title, и добавляет в них обработчики
* события onmouseover. Обработчик события выполняет HEAD-запрос с помощью
* объекта XMLHttpRequest, чтобы получить сведения о ресурсе, на который
* указывает ссылка, и сохраняет эту информацию в атрибуте title ссылки,
* благодаря чему эта информация будет отображаться во всплывающей подсказке.
*/
whenReady(function() {
  // Поддерживается ли возможность выполнения междоменных запросов?
  var supportsCORS = (new XMLHttpRequest()).withCredentials !== undefined;
  // Обойти в цикле все ссылки в документе
  var links = document.getElementsByTagName('a');
  for(var і = 0; і < links.length; і++) {
    var link = links[і];
    if (!link.href) continue; // Пропустить якоря, не являющиеся ссылками
    if (link.title) continue; // Пропустить ссылки с атрибутом title
    // Если это междоменная ссылка
    if (link.host!==location.host || link.protocol !== location.protocol)
    {
      link.title = "Ссылка на другой сайт"; // Предполагается, что нельзя
                                            // получить дополнительную информацию
      if (!supportsCORS) continue; // Пропустить, если заголовок
                                   // C0RS не поддерживается
      // Иначе есть надежда получить больше сведений о ссылке. Поэтому регистрируем
      // обработчик события, который предпримет попытку сделать это.
    }
    // Зарегистрировать обработчик события, который получит сведения
    // о ссылке при наведении на нее указателя мыши
    if (link.addEventListener)
      link.addEventListener("mouseover", mouseoverHandler, false);
    else
      link.attachEvent("onmouseover", mouseoverHandler);
  }
  function mouseoverHandler(e) {
    var link = e.target || e.srcElement; // Элемент <a>
    var url = link, liref; // URL-адрес ссылки
    var req = new XMLHttpRequest(); // Новый запрос
    req.open("HEAD", url); // Запросить только заголовки
    req.onreadystatechange = function() { // Обработчик события
      if (req.readyState !== 4) return; // Игнорировать незаверш. запросы
      if (req.status === 200) { // В случае успеха
        var type = req.getResponseHeader("Content-Type"); //Получить
        var size = req.getResponseHeader("Content-Length"); //сведения
        var date = req.getResponseHeader("Last-Modified"); //о ссылке
        // Отобразить сведения во всплывающей подсказке,
        link.title = "Тип: " + type + " n" +
                     "Размер: " + size + " n" + "Дата: " + date;
      }
      else {
        // Если запрос не удался и подсказка для ссылки еще не содержит текст
        // "Ссылка на другой сайт", вывести сообщение об ошибке,
        if (!link.title)
          link.title = "Невозможно получить сведения: n" + req.status + " " + req.statusText;
      }
    };
    req.send(null);
    // Удалить обработчик: попытка получить сведения выполняется только один раз.
    if (link.removeEventListener)
      link.removeEventListener("mouseover", mouseoverHandler, false);
    else
      link.detachEvent("onmouseover", mouseoverHandler);
  }
});

содержание 18.2. Выполнение HTTP-запросов с помощью <script>: JSONP содержание

В начале этой главы упоминалось, что элемент <script> можно использовать в качестве Ajax-транспорта: достаточно установить атрибут src элемента <script> (и вставить его в документ, если он еще не внутри документа), и браузер сгенерирует HTTP-запрос, чтобы загрузить содержимое ресурса по указанному URL-адресу. Основная причина, почему элементы <script> являются удобным Ajax-транспортом, состоит в том, что они не являются субъектами политики общего происхождения и их можно использовать для запроса данных с других серверов. Вторая причина состоит в том, что элементы <script> автоматически декодируют (т. е. выполняют) тело ответа, содержащего данные в формате JSON.

Прием использования элемента <script> в качестве Ajax-транспорта известен под названием JSONP: это способ организации взаимодействий, когда в теле ответа на HTTP-запрос возвращаются данные в формате JSON. Символ «Р» в аббревиатуре означает «padding», или «prefix» (дополнение или приставка), но об этом чуть ниже.[51]

Представьте, что вы пишете службу, которая обрабатывает GET-запросы и возвращает данные в формате JSON. Документы с общим происхождением могут пользоваться этой службой с помощью объекта XMLHttpRequest и метода JSON.parse(), как показано в примере 18.3. Если на сервере будет настроена отправка заголовка «CORS», сторонние документы при отображении в новых браузерах также смогут пользоваться службой с помощью объекта XMLHttpRequest. Однако при отображении в старых браузерах, не поддерживающих заголовок «CORS», сторонние документы смогут получить доступ к службе только с помощью элемента <script>. Ответ в формате JSON является (по определению) допустимым программным кодом на языке JavaScript, и браузер выполнит его при получении. При выполнении данных в формате JSON происходит их декодирование, но полученный результат является обычными данными, которые ничего не делают.

Именно здесь на сцену выходит символ «Р» из аббревиатуры JSONP. Когда обращение к службе реализовано с помощью элемента <script>, служба должна «дополнить» ответ, окружив его круглыми скобками и добавив в начало имя JavaScript-функции. То есть вместо того чтобы отправлять данные, такие как:

[1, 2, {"buckle": "my shoe”}]

она должна отправлять дополненные данные, как показано ниже:

handleResponse(
[1. 2. {"buckle": "my shoe"}]

Являясь телом элемента <script>, этот дополненный ответ уже будет выполнять некоторые действия: он произведет преобразование данных из формата JSON (которые в конечном итоге являются одним длинным выражением на языке JavaScript) и передаст их функции handleResponse(), которая, как предполагается, определена в теле документа и выполняет некоторые операции с данными.

Сценарии и безопасность

Используя элемент <script> в качестве Ajax-транспорта, вы разрешаете своей веб-странице выполнять любой программный код на языке JavaScript, который отправит удаленный сервер. Это означает, что прием, описываемый здесь, не должен использоваться при работе с серверами, не вызывающими доверия. Впрочем, даже при работе с доверенным сервером следует помнить, что этот сервер может быть взломан злоумышленником, и злоумышленник сможет заставить вашу веб-страницу выполнить любой программный код и отобразить любую информацию, какую он пожелает, и эта информация будет выглядеть, как если бы она поступила с вашего сайта.

При этом следует отметить, что для веб-сайтов стало обычным делом использовать доверенные сценарии сторонних разработчиков, особенно сценарии, используемые для внедрения в страницу рекламы или «виджетов». Использование элемента <script> в качестве Ajax-транспорта для взаимодействия с доверенными веб-службами ничуть не опаснее.

Чтобы этот прием действовал, необходимо иметь некоторый способ сообщить службе, что обращение к ней выполняется с помощью элемента <script> и она должна возвращать ответ не в формате JSON, а в формате JSONP. Это можно сделать, указав параметр запроса в адресе URL, например, добавив в конец ?jsonp (или &jsonp).

На практике веб-службы, поддерживающие JSONP, не диктуют имя функции, такое как «handleResponse», которую должны реализовать все клиенты. Вместо этого они используют значение параметра запроса, позволяя клиентам самим указывать имя функции, и применяют его для дополнения ответа. В примере 18.14 для определения имени функции обратного вызова используется параметр «jsonp». Многие службы, поддерживающие JSONP, распознают параметр с эти именем. Также достаточно часто используется параметр с именем «callback», и вы можете изменить программный код примера, чтобы он смог работать со службами, предъявляющими определенные требования.

Пример 18.14 определяет функцию get JS0NP(), которая выполняет запрос на получение данных в формате JSONP. В этом примере есть несколько тонкостей, о которых следует сказать особо. Во-первых, обратите внимание, что пример создает новый элемент <script>, устанавливает его URL-адрес и вставляет элемент в документ. Операция вставки вызывает выполнение HTTP-запроса. Во-вторых, для каждого запроса в этом примере создается новая внутренняя функция обратного вызова, которая сохраняется в свойстве самой функции getJSONP(). Наконец, эта функция обратного вызова выполняет необходимые заключительные операции: она удаляет элемент <script> и саму себя.

Пример 18.14. Выполнение запросов на получение данных в формате JSONP с помощью элемента <script>

// Выполняет запрос по указанному URL-адресу на получение данных в формате JSONP и передает 
// полученные данные указанной функции обратного вызова. Добавляет в URL параметр запроса 
// с именем ''jsonp'', чтобы указать имя функции обратного вызова, 
function getJS0NP(иrl, callback) {
  // Создать для данного запроса функцию с уникальным именем
  var cbnum = "cb" + getJSONP.counter++; // Каждый раз увеличивать счетчик
  var cbname = "getJSONP.” + cbnum; // Как свойство этой функции
  // Добавить имя функции в строку запроса url, используя формат представления данных 
  // HTML-форм. Здесь используется параметр с именем "jsonp". Некоторые веб-службы 
  // с поддержкой JSONP могут использовать параметр с другим именем, таким как "callback",
  if (url.indexOfC?") === -1)  // URL еще не имеет строки запроса
    url += "?jsonp=" + cbname; // добавить параметр как строку запроса
  else                         // В противном случае
    url += "&jsonp=" + cbname; // добавить как новый параметр.
  // Создать элемент script, который отправит запрос
  var script = document.createElement("script");
  // Определить функцию, которая будет вызвана сценарием
  getJSONP[cbnum] = function(response) {
    try {
      callback(response); // Обработать данные
    }
    finally { // Даже если функция или ответ возбудит исключение
      delete getJSONP[cbnum];                // Удалить эту функцию
      script.parentNode.removeChild(script); // Удалить элемент script
    }
  };
  // Инициировать HTTP-запрос
  script.src = url; // Указать url-адрес элемента
  document.body.appendChild(script); // Добавить в документ
}
getJSONP.counter = 0; // Счетчик, используемый для создания уникальных имен

содержание 18.3. Архитектура Comet на основе стандарта «Server-Sent Events»содержание

Проект стандарта «Server-Sent Events» определяет объект EventSource, который делает практически тривиальным создание приложений с архитектурой Comet. При его использовании достаточно передать URL-адрес конструктору EventSource() и затем обрабатывать события «message» в полученном объекте:

var ticker = new EventSourcefstockprices. php”);
ticker.onmessage = function(e) {
  var type = e.type;
  var data = e.data;
// Обработать строки type и data.
}

Объект события «message» имеет свойство data, хранящее строку, отправленную сервером с этим событием в качестве полезной нагрузки. Кроме того, объект события имеет свойство type, как и все другие объекты событий. По умолчанию это свойство имеет значение «message», но источник события может указать в этом свойстве другую строку. Все события от данного сервера, источника событий, обрабатываются единственным обработчиком onmessage, который при необходимости может передавать их другим обработчикам, опираясь на свойство type объекта события.

Протокол обмена, определяемый стандартом «Server-Sent Event», достаточно прост. Клиент устанавливает соединение с сервером (когда создает объект EventSource), а сервер сохраняет это соединение открытым. Когда происходит событие, сервер передает через соединение текстовую строку. Передача события через сеть выглядит примерно следующим образом:

event: bid установка свойства type объекта события
data: G00G установка свойства data
data: 999 добавляется перевод строки и дополнительные данные
пустая строка генерирует событие message

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

Одно из очевидных применений архитектуры Comet - реализация чатов: клиент может посылать в чат новые сообщения с помощью объекта XMLHttpRequest и подписываться на поток сообщений, поступающих от собеседников, с помощью объекта EventSource. Пример 18.15 демонстрирует, насколько просто реализовать клиента на основе объекта EventSource.

Пример 18.15. Простой клиент чата на основе объекта EventSource

<script>
window.onload = function() {
  // Позаботиться о некоторых особенностях пользовательского интерфейса
  var nick = prompt("Введите ваше имя"); // Получить имя пользователя
  var input = document.getElementById("input"); // Отыскать поле ввода
  input.focus(); // Передать фокус ввода
  // Подписаться на получение новых сообщений с помощью объекта EventSource
  var chat = new EventSource("/chat");
  chat.onmessage = function(event) {         // Получив новое сообщение,
    var msg = event.data;                    // Извлечь текст
    var node = document.createTextNode(msg); // Преобр. в текстовый узел
    var div = document.createElementC'div"); // Создать <div>
    div.appendChild(node);                   // Добавить текст, узел в div
    document.body.insertBefore(div, input);  // И добавить div перед input
    input.scrollIntoView();                  // Прокрутить до появления
  }                                          // input в видимой области
  // Передавать сообщения пользователя на сервер с помощью XMLHttpRequest
  input.onchange = function() { // При нажатии клавиши Enter
    var msg = nick + ": " + input.value; // Имя пользователя + введ. текст
    var xhr = new XMLHttpRequest();      // Создать новый XHR
    xhr.open("POST", "/chat");           // POST-запрос к /chat,
    xhr.setRequestHeader("Content-Type", // Тип - простой текст UTF-8
                      "text/plain;charset=UTF-8");
    xhr.send(msg);                       // Отправить сообщение
    input.value = "";                    // Приготовиться к вводу
  }                                      // следующего сообщения
};
</script>
<!-- Пользовательский интерфейс чата состоит из единственного поля ввода -->
<!-- Новые сообщения, отправленные в чат, вставляются перед полем ввода -->
<input/>

На момент написания этих строк объект EventSource поддерживался в Chrome и Safari и должен был быть реализован компанией Mozilla в первом же выпуске Firefox, вышедшем после версии 4.0. В браузерах (таких как Firefox), где реализация объекта XMLHttpRequest возбуждает событие «readystatechange» в ходе загрузки ответа (для значения 3 в свойстве readyState) многократно, поведение объекта EventSource относительно легко имитировать с помощью объекта XMLHttpRequest, как демонстрируется в примере 18.16. С модулем имитации пример 18.15 будет работать в Chrome, Safari и Firefox. (Пример 18.16 не будет работать в браузерах ІE и Opera, поскольку реализации объекта XMLHttpRequest в этих браузерах не генерируют события в ходе загрузки.)

Пример 18.16. Имитация объекта EventSource с помощью XMLHttpRequest

// Имитация прикладного интерфейса EventSource в браузерах, не поддерживающих его.
// Требует, чтобы XMLHttpRequest генерировал многократные события readystatechange 
// в ходе загрузки данных из долгоживущих HTTP-соединений. Учтите, что это не полноценная 
// реализация API: она не поддерживает свойство readyState, метод close(), а также 
// события open и error. Кроме того, регистрация обработчика события message выполняется 
// только через свойство onmessage -- эта версия не определяет метод addEventListener
if (window.EventSource === undefined) { // Если EventSource не поддерживается,
  window.EventSource = function(url) { // использовать эту его имитацию,
    var xhr; // HTTP-соединение...
    var evtsrc = this: // Используется в обработчиках,
    var charsReceived = 0; // Так определяется появление нового текста
    var type = null; // Для проверки свойства type ответа,
    var data = ""; // Хранит данные сообщения
    var eventName = "message": // Значение поля type объектов событий
    var lastEventld = ""; // Для синхронизации с сервером
    var retrydelay = 1000: // Задержка между попытками соединения
    var aborted = false: // Установите в true, чтобы разорвать соединение
    // Создать объект XHR
    xhr = new XMLHttpRequest():
    // Определить обработчик события для него
    xhr.onreadystatechange = function() {
      switch(xhr.readyState) {
      case 3: processData(): break; // При получении фрагмента данных
      case 4: reconnectO: break; // По завершении запроса
      }
    };
    // И установить долгоживущее соединение
    connect();
    // Если соединение было закрыто обычным образом, ждать секунду 
    // и попробовать восстановить соединение
    function reconnect() {
      if (aborted) return; // He восстанавливать после
                         // принудительного прерывания
      if (xhr.status >= 300) return; 
      // He восстанавливать после ошибки
      setTimeout(connect, retrydelay); // Ждать и повторить попытку
    };
    // Устанавливает соединение
    function connect() {
      charsReceived = 0; type = null; xhr.open("GET", url); 
      xhr.setRequestHeader(“Cache-Control", "no-cache");
      if (lastEventld) 
        xhr.setRequestHeader("Last-Event-ID", lastEventld);
     xhr.send(); 
    } 
    // При получении данных обрабатывает их и вызывает обработчик onmessage.
    // Эта функция реализует работу с протоколом Server-Sent Events 
    function processData() { 
      if (!type) { // Проверить тип ответа, если это еще не сделано
        type = xhr.getResponseHeader(’Content-Type’);
        if (type !== "text/event-stream") {
          aborted = true;
          xhr.abort();
          return; 
        } 
      } 
      // Запомнить полученный объем данных и извлечь только ту часть ответа,
      // которая еще не была обработана,
      var chunk = xhr.responseText.substring(charsReceived);
      charsReceived = xhr.responseText.length; 
      // Разбить текст на строки и обойти их в цикле. 
      var lines = chunk.replace(/(rn|r|n)$/,"").split(/rn|r|n/); 
      for(var і = 0; і < lines.length; i++) { 
        var line = lines[i], pos = line.indexOf(":"), name.value="";
        if (pos == 0) continue; // Игнорировать комментарии 
        if (pos > 0) { // поле name:value 
          name = line.substring(0,pos);
          value = line.substring(pos+1); 
          if (value.charAt(O) == " ") value = value.substrings); 
        } 
        else name = line; // только поле name 
        switch(name) { 
        case "event": eventName = value; break; 
        case "data": data += value + "n"; break;
        case "id": lastEventld = value; break;
        case "retry": retrydelay = parselnt(value) || 1000; break;
        default: break; // Игнорировать любые другие строки 
        } 
        if (line === "") { // Пустая строка означает отправку события
          if (evtsrc.onmessage && data !== "") { 
            // Отсечь завершающий символ перевода строки
            if (data.charAt(data.length-1) == "n")
            data = data.substrings, data.length-1);
            evtsrc.onmessage({ // Имитация объекта Event 
                   type: eventName, // тип события 
                   data: data, // данные 
                   origin: url // происхождение данных 
            });
          }
          data = "";
          continue;
        }
      }
    }
  };
}

Завершим описание архитектуры Comet примером серверного сценария. В примере 18.17 приводится реализация HTTP-сервера на серверном JavaScript, который выполняется под управлением интерпретатора Node (раздел 12.2). Когда клиент обращается к корневому URL «/», сервер отправляет ему реализацию клиента чата, представленную в примере 18.15, и реализацию имитации, представленную в примере 18.16. Когда клиент выполняет GET-запрос по URL-адресу «/chat», сервер сохраняет поток ответа в массиве и поддерживает соединение открытым. А когда клиент выполняет POST-запрос к адресу «/chat», сервер интерпретирует тело запроса как текст сообщения и добавляет префикс «data:», как того требует протокол Server-Sent Events, во все открытые потоки сообщений. Если вы установите интерпретатор Node, вы сможете запустить этот пример сервера локально. Он прослушивает порт 8000, поэтому после запуска сервера в браузере необходимо будет указать адрес http://localhost:8000, чтобы соединиться с сервером и начать общение с самим собой.

Пример 18.17. Сервер чата, поддерживающий протокол Server-Sent Events

// Этот программный код на серверном JavaScript предназначен для выполнения
// под управлением NodeJS. Он реализует очень простую, полностью анонимную комнату чата.
// Для отправки новых сообщений в чат следует использовать POST-запросы к URL /chat,
// а для получения текста/потока-событий сообщений следует использовать GET-запросы
// к тому же URL. При выполнении GET-запроса к / возвращается простой HTML-файл,
// содержащий пользовательский интерфейс чата для клиента.
var http = require('http'); // Реализация API HTTP-сервера в NodeJS
// HTML-файл для клиента чата. Используется ниже.
var clientui = require('fs').readFileSync( "chatclient.html");
var emulation = require( fs').readFileSync("EventSourceEmulation.js");
// Массив объектов ServerResponse, который будет использоваться для отправки событий 
var clients = [];
// Отправлять комментарий клиентам каждые 20 секунд, чтобы предотвратить
// закрытие соединения с последующими попытками восстановить его 
setInterval(function() {
  clients.forEach(function(client) {
    client. write('': pingn");
  });
}, 20000);
// Создать новый сервер
var server = new http.Server();
// Когда сервер получит новый запрос, он вызовет эту функцию 
server.on( "request", function (request, response) {
  // Проанализировать запрошенный URL
  var url = require(’url').parse(request.url);
  // Если запрошен URL "/", отправить пользовательский интерфейс чата,
  if (url.pathname === "/") { // Запрошен пользовательский интерфейс чата
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("<script>" + emulation + "</script>");
    response.write(clientui);
    response.end();
    return;
  }
  // Если запрошен любой другой URL, кроме "/chat", отправить код 404
  else
    if (url.pathname !== "/chat") {
    response.writeHead(404);
    response.end();
    return;
  }
  // Если был выполнен POST-запрос - клиент отправил новое сообщение
  if (request.method === "POST") {
    request.setEncoding("utf8");
    var body = "";
    // При получении фрагмента данных добавить его в переменную body
    request.on("data", function(chunk) { body += chunk; });
    // По завершении запроса отправить пустой ответ
    // и широковещательное сообщение всем клиентам,
    request.on("end", function() {
      response.writeHead(200); // Ответ на запрос
      response.end();
      // Преобразовать сообщение в формат текст/поток-событий.
      // Убедиться, что все строки начинаются с префикса "data:"
      // и само сообщение завершается двумя символами перевода строки,
      message = 'data: ' + body, replace('n’, 'ndata: ') + "rnrn";
      // Отправить сообщение всем клиентам
      clients.forEach(function(client) { client.write(message); });
    });
  }
  // Если иначе, клиент запросил поток сообщений
  else {
    // Установить тип содержимого и отправить начальное событие message 
    response.writeHead(200, {'Content-Type': "text/event-stream" });
    response.write("data: Connectednn");
    // Если клиент закрыл соединение, удалить соответствующий
    // объект response из массива активных клиентов
    request.connection.on("end", function() {
      clients.splice(clients.index0f(response), 1);
      response.end();
    }):
    // Запомнить объект response, чтобы в дальнейшем посылать сообщения с его помощью 
    clients.push(response);
  }
});
// Запустить сервер на порту 8000. Чтобы подключиться к нему, используйте
// адрес http://localhost:8000/
server.listen(8000);