Xmlhttprequest обработка ошибок

Время на прочтение
4 мин

Количество просмотров 10K

Если вы пришли сюда только ради ответа и вам не интересны рассуждения — листайте вниз :)

Как все начиналось

Для начала, давайте вспомним, а как вообще ловят ошибки в js, будь то браузер или сервер. В js есть конструкция try...catch.

try {
    let data = JSON.parse('...');
} catch(err: any) {
		// если произойдет ошибка, то мы окажемся здесь
}

Это общепринятая конструкция и в большинстве языков она есть. Однако, тут есть проблема (и как окажется дальше — не единственная), эта конструкция «не будет работать» для асинхронного кода, для кода который был лет 5 назад. В те времена, в браузере использовали для Ajax запроса XMLHttpRequest.

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com', true);
xhr.addEventListener('error', (e: ProgressEvent<XMLHttpRequestEventTarget>) => {
    // если произойдет ошибка, то мы окажемся здесь
});

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

В NodeJS с самого начала продвигалась концепция Error-First Callback, эта идея применялась для асинхронных функций, например, для чтения файла. Смысл ее в том, чтобы первым аргументом передавать в функцию обратного вызова ошибку, а следующими аргументами уже получаемые данные.

import fs from 'fs';

fs.readFile('file.txt', (err, data) => {
    if (err) {
        // обработка ошибки
    }
    // если все хорошо, работаем с данными
});

Если мы посмотрим какой тип имеет переменная err, то увидим следующее:

interface ErrnoException extends Error {
    errno?: number | undefined;
    code?: string | undefined;
    path?: string | undefined;
    syscall?: string | undefined;
}

Тут действительно находится ошибка. По сути, это тот же способ, что и выше, только в этом случает мы получаем объект Error.

Через некоторое время, в Javascript появились Promise. Они, безусловно, изменили разработку на js к лучшему. Ведь никто* никто не любит городить огромные конструкции из функций обратного вызова.

fetch('https://api.example.com')
  .then(res => {
    // если все хорошо, работаем с данными
  })
  .catch(err => {
		// обработка ошибки
  });

Несмотря на то, что внешне этот пример сильно отличается от первого, тем не менее, мы видим явную логическую связь. Очевидно, что разработчики хотели сделать похожую на try...catch конструкцию. Со временем, появился еще один способ обработать ошибку в асинхронном коде. Этот способ, по сути, является лишь синтаксическим сахаром для предыдущего примера.

try {
  const res = await fetch('https://api.example.com');
  // если все хорошо, работаем с данными
} catch(err) {
	// обработка ошибки
}

Также, конструкция try...catch позволяет ловить ошибки из нескольких промисов одновременно.

try {
  let usersRes = await fetch('https://api.example.com/users');
	let users = await usersRes.json();

  let chatsRes = await fetch('https://api.example.com/chats');
	let chats = await chatsRes.json();

  // если все хорошо, работаем с данными
} catch(err) {
	// обработка ошибки
}

Вот, замечательный вариант ловли ошибок. Любая ошибка которая возникнет внутри блока try, попадет в блок catch и мы точно её обработаем.

А точно ли обработаем?

Действительно, а правда ли, что мы обработаем ошибку, или всего лишь сделаем вид? На практике, скорее всего, возникнувшая ошибка будет просто выведена в консоль или т.п. Более того, при появлении ошибки*, интерпретатор прыгнет в блок catch , где не мы, не TypeScript не сможет вывести тип переменной, попавшей туда (пример — возврат с помощью Promise.reject), после чего, произойдет выход из функции. То есть, мы не сможем выполнить код который находится в этом же блоке, но который расположен ниже функции, внутри которой произошла ошибка. Конечно, мы можем предусмотреть такие ситуации, но сложность кода и читаемость вырастут многократно.

Как быть?

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

let [users, err] = await httpGET('https://api.example.com/users');
if (err !== null) {
	// обработка ошибки
}
// продолжаем выполнение кода

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

Пример для вызова нескольких функций возвращающих Promise.

let err: Error,
		users: User[],
		chats: Chat[];

[users, err] = await httpGET('https://api.example.com/users');
if (err !== nil) {
  // обработка ошибки
}

[chats, err] = await httpGET('https://api.example.com/chats');
if (err !== nil) {
  // обработка ошибки
}

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

Давайте рассмотрим как можно реализовать такую функцию и что нам вообще нужно делать. Для начала, давайте определим тип PairPromise. В данном случае, я решил использовать null если результата или ошибки нету, так как он просто короче.

type PairPromise<T> = Promise<[T, null] | [null, Error]>;

Определим возможные возвращаемые ошибки.

const notFoundError = new Error('NOT_FOUND');
const serviceUnavailable = new Error('SERVICE_UNAVAILABLE');

Теперь опишем нашу функцию.

const getUsers = async (): PairPromise<User[]> => {
    try {
        let res = await fetch('https://api.example.com/users');
        if (res.status === 504) {
            return Promise.resolve([null, serviceUnavailable]);
        }

        let users = await res.json() as User[];

        if (users.length === 0) {
            return Promise.resolve([null, notFoundError]);
        }

        return Promise.resolve([users, null]);
    } catch(err) {
        return Promise.resolve([null, err]);
    }
} 

Пример использования такой функции.

let [users, err] = await getUsers();
if (err !== null) {
	switch (err) {
  	case serviceUnavailable:
    	// сервис недоступен
    case notFoundError:
    	// пользователи не найдены
    default:
    	// действие при неизвестной ошибке
	}
}

Вариантов применения данного подхода обработки ошибок очень много. Мы сочетаем удобства конструкции try...catch и Error-First Callback, мы гарантированно поймаем все ошибки и сможем удобно их обработать, при необходимости. Как приятный бонус — мы не теряем типизацию. Также, мы не скованы лишь объектом Error, мы можем возвращать свои обертки и успешно их использовать, в зависимости от наших убеждений.

Очень интересно мнение сообщества на эту тему.

I want to catch all errors with the help of TRY {} CATCH(){} when I send data to a server via XMLHttpRequest.

How can I receive all errors, such as net::ERR_INTERNET_DISCONNECTED, etc. ?

Paul's user avatar

Paul

26k12 gold badges84 silver badges119 bronze badges

asked Nov 5, 2014 at 12:02

Hit-or-miss's user avatar

2

Try catches didn’t work for me. I personally ended up testing for response == «» and status == 0.

        var req = new XMLHttpRequest();
        req.open("post", VALIDATE_URL, true);
        req.onreadystatechange = function receiveResponse() {

            if (this.readyState == 4) {
                if (this.status == 200) {
                    console.log("We got a response : " + this.response);
                } else if (!isValid(this.response) && this.status == 0) {
                    console.log("The computer appears to be offline.");
                }
            }
        };
        req.send(payload);
        req = null;

answered Apr 30, 2015 at 22:03

Francois Dermu's user avatar

Francois DermuFrancois Dermu

4,4272 gold badges22 silver badges13 bronze badges

4

Refer this,

function createXMLHttpRequestObject()
{
  // xmlHttp will store the reference to the XMLHttpRequest object
  var xmlHttp;
  // try to instantiate the native XMLHttpRequest object
  try
  {
    // create an XMLHttpRequest object
    xmlHttp = new XMLHttpRequest();
  }
  catch(e)
  {
        try
    {
      xmlHttp = new ActiveXObject("Microsoft.XMLHttp");
    }
    catch(e) { }
  }
  // return the created object or display an error message
  if (!xmlHttp)
    alert("Error creating the XMLHttpRequest object.");
  else 
    return xmlHttp;
}

answered Nov 5, 2014 at 12:07

soundhiraraj's user avatar

2

You should put all the statements which you think that will cause an exception in a try block. After that you can give several catch statements — each one for one exception. In last you can give finally as well — this statement will be executed after Try block regardless of whether or not exception was thrown or caught.

Syntax can be like this:

try{
try_statements
}

[catch (exception_var_2) { catch_statements_1 }]
[catch (exception_var_2) { catch_statements_2 }]
...
[catch (exception_var_2) { catch_statements_N }]

[finally { finally_statements }]

Example:

try {
   myroutine(); // may throw three exceptions
} catch (e if e instanceof TypeError) {
   // statements to handle TypeError exceptions
} catch (e if e instanceof RangeError) {
   // statements to handle RangeError exceptions
} catch (e if e instanceof EvalError) {
   // statements to handle EvalError exceptions
} catch (e) {
   // statements to handle any unspecified exceptions
   logMyErrors(e); // pass exception object to error handler
}

You can read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try…catch

answered Nov 5, 2014 at 12:11

disp_name's user avatar

disp_namedisp_name

1,4282 gold badges20 silver badges46 bronze badges

1

В интерактивности — статические html сайты — это прошлое. Динамические с использованием CGI (или модулей сервера, например Apache) и баз данных, когда сервер при отправки формы формирует страницу и показывает ее после обновления — чуть современней, но все же во многих областях, где требуется практически сопостовимая с десктопными приложениями интерактивность — так же угасают. На смену приходят интерактивные функциональные программы, в полной мере взаимодействующие с пользователем; информация, полученная от сервера практически мгновенно отображается на экране без перезагрузке страницы. Речь я виду об AJAX’e, что в расшифровке «асинхронный JavaScript и XML» (термин ввел Джесс Гарретт). А если более подробно, то — «асинхронный JavaScript + CSS + DOM + XMLHttpRequest».

Методы объекта XMLHttpRequest

Все нижеизложанные методы и свойства — общие для Internet Explorer 5, Mozilla, Netscape 7, и соответственно, использовать их можно безопасно.

abort()
обрывает текущий запрос

getAllResponseHeaders()
возвращает полный набор заголовков ответа (названий и значений) в виде строки

getResponseHeader(<headerLabel>)
возвращает строковое значение заголовка, название которого указано в параметре .

open(<method>, <URL> [, <asyncFlag>[, <userName>[, <password>]]])
Присвоение параметров (метода, URL, и других) текущему запросу.

send(<content>)
Посылает запрос

setRequestHeader(<label>, <value>)
Установка в отправляемом запросе заголовка <label> со значением <value>

Свойства объекта XMLHttpRequest

onreadystatechange
событие, возникающее при смене статуса объекта

readyState
значения статуса (integer), может принимать следующие значения: 0 = неинициализирован (uninitialized); 1 = «идет загрузка» (loading); 2 = «загружен» (loaded); 3 = «интерактивен» (interactive) 4 = «выполнен» (complete)

responseText
строка с возвращенными сервером данными

responseXML
DOM-совместимый объект-документ с возвращенными сервером данными

status
стандартный HTTP код статуса, например 404 (для «Not Found») или 200 (для «OK»)

statusText
текстовое сообщение статуса

Здесь все необходимые свойства и методы этого объекта, которые помогут нам решить наш таск. Опишем последовательность наших действий:

Алгоритм:

1. Создание экземпляра объекта XMLHttpRequest.
2. Объявление обработчика события onreadystatechange нашего экземпляра .
3. Открытие соединения с указанием типа запроса, URL и других параметров.
4. Посыл запроса.

Алгоритм незамысловат, но, учитывая кое-какие нюансы (и учитывая, что мы учимся :)), конечно же, рассмотрим его подробней:

Итак, пункт первый — создание экземпляра объекта. Вот здесь всплывает особенность обеспечения кроссбраузерности. Конструкция создания объекта различна: в IE 5+ она реализована через ActiveXObject, а в остальных браузерах (Mozilla, Netscape и Safari) — как встроенный объект типа XMLHttpRequest.

Для Internet Explorer:

var request = new ActiveXObject("Microsoft.XMLHTTP");

Для всех остальных:

var request = new XMLHttpRequest();

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

Далее по плану — создание обработчика событий и открытие соединения. Это весьма просто:

request.onreadystatechange = processRequestChange;
request.open("GET", url, false);

Здесь мы используем метод GET, хотя можно и POST; в общем виде это выглядет так: request.open(<«GET»|»POST»|…>, <url>, <asyncFlag>);. Функцию, являющуюся обработчиком события onreadystatechange (в нашем случае это функция — processRequestChange()), мы должны определить сами.

Ну и последний пункт — посыл запроса — метод send() (для версии без ActiveX в качестве параметра нужно передать null).

// для IE
request.send();

// для остальных
request.send(null);

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

Исходя из всего вышесказанного, JavaScript код будет примерно следущим:

var request;

/**
* Load XMLDoc function
* Здесь в качестве параметра url при вызове мы должны указать
* backend-скрипт, который, собственно, и получит данные с сервера
*/

function doLoad(url) {
 if (window.XMLHttpRequest) {
 request = new XMLHttpRequest();
 request.onreadystatechange = processRequestChange;
 request.open("GET", url, true);
 request.send(null);
 } else if (window.ActiveXObject) {
 request = new ActiveXObject("Microsoft.XMLHTTP");
 if (request) {
 request.onreadystatechange = processRequestChange;
 request.open("GET", url, true);
 request.send();
 }
 }
}

/**
* Get request state text function
*/
function getRequestStateText(code) {
 switch (code) {
 case 0: return "Uninitialized."; break;
 case 1: return "Loading..."; break;
 case 2: return "Loaded."; break;
 case 3: return "Interactive..."; break;
 case 4: return "Complete."; break;
 }
}

/**
* Event on request change
* Собственно, обработчик события onreadystatechange.
* Здесь мы, в зависимости от состояния запроса,
* будем скрывать / показывать слои "Загрузка данных",
* само поле данных и т.д.
*/
function processRequestChange() {
 document.getElementById("resultdiv").style.display = 'none';
 document.getElementById("state").value = getRequestStateText(request.readyState);
 abortRequest = window.setTimeout("request.abort();", 10000);
 // если выполнен
 if (request.readyState == 4) {
 clearTimeout(abortRequest);
 document.getElementById("statuscode").value = request.status;
 document.getElementById("statustext").value = request.statusText;
 // если успешно
 if (request.status == 200) {
 document.getElementById("resultdiv").style.display = 'block';
 document.getElementById("responseHTML").innerHTML = request.responseText;
 } else {
 alert("Не удалось получить данные:n" + request.statusText);
 }
 document.getElementById("loading").style.display = 'none';
 }
 // иначе, если идет загрузка или в процессе - показываем слой "Загружаются данные"
 else if (request.readyState == 3 || request.readyState == 1) {
 document.getElementById("loading").style.display = 'block';
 }
}

Теперь HTML-формы нашего примера:

<input type="text"
 id="search"
 value="Введите первые буквы ника"
 onFocus="this.value=''; document.getElementById('resultdiv').style.display='none';"
/>
<input type="button"
 value="Поиск"
 onClick="doLoad('ajaxsearch.php?search='+document.getElementById('search').value);" 
/><br /><br />

Дополнительная информация о выполнении запроса:<br /><br />
<table>
 <tr>
 <td>Состояние запроса:</td>
 <td><input type="text" id="state" disabled="true" /></td>
 </tr>
 <tr>
 <td>Код статуса:</td>
 <td>
 <input type="text" id="statuscode" disabled="true" />
 <input type="text" id="statustext" disabled="true" />
 </td>
 </tr>
</table>

Обратите внимение на фрагмент, выделенный зеленным цветом — событие onClick кнопки «Поиск». Мы вызываем функицю doLoad(…), в качестве параметра которой передаем адрес backend-скрипта, выполняющего поиск в базе зарегистрированного пользователя. О backend-скрипе чуть позже, имя его мы определили как ajaxsearch.php. Также GET-параметром скрипту мы передаем переменную search, со значением, взятым из поля ввода для ника.

И, как было сказано выше, объявим дополнительные HTML-элементы (в нашем случае — это невидимые слои) для отображения полученного содержимого и окна загрузки с возможностью отмены:

<div id="resultdiv" style="display: none;">
 Резульаты поиска:
 <span id="responseHTML"></span>
</div>

<div id="loading"
 style="
 position: absolute;
 top: 450px;
 left: 550px;
 display: none;
 width: 125px;
 height: 40px;
 font-family: Verdana;
 font-size: 11pt;
 border: 1px solid #BBBBBB;
 background: #EEEEEE;
 padding: 5px 5px 5px 5px;
 "
>
 Loading data...
 <div id="canselloading"
 style="
 background: red;
 border: 1px solid #000000;
 color: #FFFFFF;
 padding: 2px 2px 2px 2px;
 cursor: pointer;
 "
 onClick="
 request.abort();
 document.getElementById('loading').style.display = 'none';
 return false;
 "
 >Cansel
 </div>
</div>

Ну что ж, с frontend’ом разобрались, переходим к backend’у — скрипт ajaxsearch.php. И вновь мы сталкиваемся с небольшими нюансами: для того, чтобы PHP-скрипт корректно работал с XMLHttpRequest, он (скрипт) должен посылать ряд заголовков. А именно: тип содержимого и его кодировку (особенно важно, если вы работаете с кириллицей), а также параметры кеширования — любое кеширование должно быть отключено (ну это и понятно — необходимо иметь свежую информацию).

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

header("Content-type: text/html; charset=windows-1251");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);

И еще одна особенность: если вы будете выводит данные в формате text/plane (в нашем случае — text/html, поэтому нас это не каснется, но все же — чтобы знать), помните, что спецсимволы такие как n, t, r и т.д., обрабатываются по умолчанию только в строках с двойными кавычками:

// т.е. правильно так
print "MessagenFrom AJAX";

// а не так!
print 'MessagenFrom AJAX';

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

<?php
 
 
 /**
 * Посыл заголовков
 */
 
 header("Content-type: text/plain; charset=windows-1251");
 header("Cache-Control: no-store, no-cache, must-revalidate");
 header("Cache-Control: post-check=0, pre-check=0", false);
 
 
 /**
 * Хост, логин и пароль базы данных
 * (вам, естественно, нужно заменить на свои значения)
 */
 
 $dbhost = "localhost";
 $dblogin = "root";
 $dbpassword = "root";
 
 
 /**
 * Коннектимся к базе, выполняем
 * запрос, получаем результат
 */
 
 @mysql_connect($dbhost, $dblogin, $dbpassword) or die("Unable to connect to database..");
 @mysql_select_db("MYDATABASE") or die("Unable to select database");
 $sql = "SELECT * FROM users WHERE nick LIKE '%".$_GET["search"]."%' ORDER BY nick";
 $result = mysql_query($sql);
 
 print "Найдено по запросу: ".mysql_num_rows($result);
 
 
 /**
 * Если есть ряды, выводим таблицу
 */
 
 if (mysql_num_rows($result) > 0) {
 print "<table>";
 print "<tr>";
 print "<td>NickName</td>";
 print "<td>RealName</td>";
 print "<td>E-mail</td>";
 print "</tr>";
 
 $get = $_GET["search"];
 
 while ($row = mysql_fetch_array($result)) {
 print "<tr>";
 print "<td>";
 print ($row["unick"] ? preg_replace("/($get)/i", "<font color='red'>1</font>", $row["unick"]) : "&nbsp;");
 print "</td>";
 print "<td>($row['urealname'] ? $row['urealname'] : '&nbsp;')</td>";
 print "<td>$row['umail']</td>";
 print "</tr>";
 }
 print "</table>";
 }
 
?>

Ну вот, друзья, собственно, и все на сегодня.

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

Тим Бернес-Ли, Всемирная паутина: Очень короткая личная история

Если в адресной строке браузера набрать eloquentjavascript.net/17_http.html, браузер сначала распознает адрес сервера, связанный с именем eloquentjavascript.net и попробует открыть TCP соединение по 80 порту – порт для HTTP по умолчанию. Если сервер существует и принимает соединение, браузер отправляет что-то вроде:

GET /17_http.html HTTP/1.1

Host: eloquentjavascript.net

User-Agent: Название браузера

Сервер отвечает по тому же соединению:

Last-Modified: Wed, 09 Apr 2014 10:48:09 GMT

Браузер берёт ту часть, что идёт за ответом после пустой строки и показывает её в виде HTML-документа.

Информация, отправленная клиентом, называется запросом. Он начинается со строки:

GET /17_http.html HTTP/1.1

Первое слово – метод запроса. GET означает, что нам нужно получить определённый ресурс. Другие распространённые методы – DELETE для удаления, PUT для замещения и POST для отправки информации. Заметьте, что сервер не обязан выполнять каждый полученный запрос. Если вы выберете случайный сайт и скажете ему DELETE главную страницу – он, скорее всего, откажется.

Часть после названия метода – путь к ресурсу, к которому отправлен запрос. В простейшем случае, ресурс – просто файл на сервере, но протокол не ограничивается этой возможностью. Ресурс может быть чем угодно, что можно передать в качестве файла. Многие серверы создают ответы на лету. К примеру, если вы откроете twitter.com/marijnjh, сервер посмотрит в базе данных пользователя marijnjh, и если найдёт – создаст страницу профиля этого пользователя.

После пути к ресурсу первая строка запроса упоминает HTTP/1.1, чтобы сообщить о версии HTTP – протокола, которую она использует.

Ответ сервера также начинается с версии протокола, за которой идёт статус ответа – сначала код из трёх цифр, затем строчка.

Коды статуса, начинающиеся с 2, обозначают успешные запросы. Коды, начинающиеся с 4, означают, что что-то пошло не так. 404 – самый знаменитый статус HTTP, обозначающий, что запрошенный ресурс не найден. Коды, начинающиеся с 5, обозначают, что на сервере произошла ошибка, но не по вине запроса.

За первой строкой запроса или ответа может идти любое число строк заголовка. Это строки в виде “имя: значение”, которые обозначают дополнительную информацию о запросе или ответе. Эти заголовки были включены в пример:

Last-Modified: Wed, 09 Apr 2014 10:48:09 GMT

Тут определяется размер и тип документа, полученного в ответ. В данном случае это HTML-документ размером 65’585 байт. Также тут указано, когда документ был изменён последний раз.

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

После заголовков, как запрос, так и ответ могут указать пустую строку, за которой следует тело, содержащее передаваемые данные. Запросы GET и DELETE не пересылают дополнительных данных, а PUT и POST пересылают. Некоторые ответы, например, сообщения об ошибке, не требуют наличия тела.

Как мы видели в примере, браузер отправляет запрос, когда мы вводим URL в адресную строку. Когда в полученном HTML документе содержатся упоминания других файлов, такие, как картинки или файлы JavaScript, они тоже запрашиваются с сервера.

Веб-сайт средней руки легко может содержать от 10 до 200 ресурсов. Чтобы иметь возможность запросить их побыстрее, браузеры делают несколько запросов одновременно, а не ждут окончания запросов одного за другим. Такие документы всегда запрашиваются через запросы GET.

На страницах HTML могут быть формы, которые позволяют пользователям вписывать информацию и отправлять её на сервер. Вот пример формы:

<form method=«GET« action=«example/message.html«>

<p>Имя: <input type=«text« name=«name«></p>

<p>Сообщение:<br><textarea name=«message«></textarea></p>

<p><button type=«submit«>Отправить </button></p>

Код описывает форму с двумя полями: маленькое запрашивает имя, а большое – сообщение. При нажатии кнопки «Отправить» информация из этих полей будет закодирована в строку запроса (query string). Когда атрибут method элемента равен GET, или когда он вообще не указан, строка запроса помещается в URL из поля action, и браузер делает GET-запрос с этим URL.

GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

Начало строки запроса обозначено знаком вопроса. После этого идут пары имён и значений, соответствующие атрибуту name полей формы и содержимому этих полей. Амперсанд (&) используется для их разделения.

Сообщение, отправляемое в примере, содержит строку “Yes?”, хотя знак вопроса и заменён каким-то странным кодом. Некоторые символы в строке запроса нужно экранировать (escape). Знак вопроса в том числе, и он представляется кодом %3F. Есть какое-то неписаное правило, по которому у каждого формата должен быть способ экранировать символы. Это правило под названием кодирование URL использует процент, за которым идут две шестнадцатеричные цифры, которые представляют код символа. 3F в десятичной системе будет 63, и это код знака вопроса. У JavaScript есть функции encodeURIComponent и decodeURIComponent для кодирования и раскодирования.

console.log(encodeURIComponent(«Hello & goodbye»));

// → Hello%20%26%20goodbye

console.log(decodeURIComponent(«Hello%20%26%20goodbye»));

Если мы поменяем атрибут method в форме в предыдущем примере на POST, запрос HTTP с отправкой формы пройдёт при помощи метода POST, который отправит строку запроса в теле запроса, вместо добавления её к URL.

POST /example/message.html HTTP/1.1

Content-type: application/x-www-form-urlencoded

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

В следующей главе мы вернёмся к формам и поговорим про то, как мы можем делать их при помощи JavaScript.

Интерфейс, через который JavaScript в браузере может делать HTTP-запросы, называется XMLHttpRequest (заметьте, как прыгает размер букв). Он был разработан в Microsoft для браузера Internet Explorer в конце 1990-х. В это время формат XML был очень популярным в мире бизнес-программ – а в этом мире Microsoft всегда чувствовал себя, как дома. Он был настолько популярным, что аббревиатура XML была пришпилена перед названием интерфейса для работы с HTTP, хотя последний с XML вообще не связан.

И всё же имя не полностью бессмысленное. Интерфейс позволяет разбирать вам ответы, как если бы это были документы XML. Смешивать две разные вещи (запрос и разбор ответа) в одну – это, конечно, отвратительный дизайн, но что поделаешь.

Когда интерфейс XMLHttpRequest был добавлен в Internet Explorer, стало можно делать вещи, которые раньше было делать очень сложно. К примеру, сайты стали показывать списки из подсказок, пока пользователь вводит что-либо в текстовое поле. Скрипт отправляет текст на сервер через HTTP одновременно с набором текста пользователем. Сервер, у которого есть база данных для возможных вариантов ввода, ищет среди записей подходящие и возвращает их назад для показа. Это выглядело очень круто – люди до этого привыкли ждать перезагрузки всей страницы после каждого взаимодействия с сайтом.

Другой важный браузер того времени Mozilla (позже Firefox), не хотел отставать. Чтобы разрешить делать сходные вещи, Mozilla скопировал интерфейс вместе с названием. Следующее поколение браузеров последовало этому примеру, и сегодня XMLHttpRequest является стандартом de facto.

Чтобы отправить простой запрос, мы создаём объект запроса с конструктором XMLHttpRequest и вызываем методы open и send.

var req = new XMLHttpRequest();

req.open(«GET», «example/data.txt», false);

console.log(req.responseText);

// → This is the content of data.txt

Метод open настраивает запрос. В нашем случае мы решили сделать GET запрос на файл example/data.txt. URL, не начинающиеся с названия протокола (например, http:) называются относительными, то есть они интерпретируются относительно текущего документа. Когда они начинаются со слеша (/), они заменяют текущий путь – часть после названия сервера. В ином случае часть текущего пути вплоть до последнего слеша помещается перед относительным URL.

После открытия запроса мы можем отправить его методом send. Аргументом служит тело запроса. Для запросов GET используется null. Если третий аргумент для open был false, то send вернётся только после того, как был получен ответ на наш запрос. Для получения тела ответа мы можем прочесть свойство responseText объекта request.

Можно получить из объекта response и другую информацию. Код статуса доступен в свойстве status, а текст статуса – в statusText. Заголовки можно прочесть из getResponseHeader.

var req = new XMLHttpRequest();

req.open(«GET», «example/data.txt», false);

console.log(req.status, req.statusText);

console.log(req.getResponseHeader(«content-type»));

Названия заголовков не чувствительны к регистру. Они обычно пишутся с заглавной буквы в начале каждого слова, например “Content-Type”, но “content-type” или “cOnTeNt-TyPe” будут описывать один и тот же заголовок.

Браузер сам добавит некоторые заголовки, такие, как “Host” и другие, которые нужны серверу, чтобы вычислить размер тела. Но вы можете добавлять свои собственные заголовки методом setRequestHeader. Это нужно для особых случаев и требует содействия сервера, к которому вы обращаетесь – он волен игнорировать заголовки, которые он не умеет обрабатывать.

В примере запрос был окончен, когда заканчивается вызов send. Это удобно потому, что свойства вроде responseText становятся доступными сразу. Но это значит, что программа наша будет ожидать, пока браузер и сервер общаются меж собой. При плохой связи, слабом сервере или большом файле это может занять длительное время. Это плохо ещё и потому, что никакие обработчики событий не сработают, пока программа находится в режиме ожидания – документ перестанет реагировать на действия пользователя.

Если третьим аргументом open мы передадим true, запрос будет асинхронным. Это значит, что при вызове send запрос ставится в очередь на отправку. Программа продолжает работать, а браузер позаботиться об отправке и получении данных в фоне.

Но пока запрос обрабатывается, мы не получим ответ. Нам нужен механизм оповещения о том, что данные поступили и готовы. Для этого нам нужно будет слушать событие “load”.

var req = new XMLHttpRequest();

req.open(«GET», «example/data.txt», true);

req.addEventListener(«load», function() {

console.log(«Done:», req.status);

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

Когда ресурс, возвращённый объектом XMLHttpRequest, является документом XML, свойство responseXML будет содержать разобранное представление о документе. Оно работает схожим с DOM образом, за исключением того, что у него нет присущей HTML функциональности навроде свойства style. Объект, содержащийся в responseXML, соответствует объекту document. Его свойство documentElement ссылается на внешний тег документа XML. В следующем документе (example/fruit.xml) таким тегом будет :

<fruit name=«banana« color=«yellow«/>

<fruit name=«lemon« color=«yellow«/>

<fruit name=«cherry« color=«red«/>

Мы можем получить такой файл следующим образом:

var req = new XMLHttpRequest();

req.open(«GET», «example/fruit.xml», false);

console.log(req.responseXML.querySelectorAll(«fruit»).length);

Документы XML можно использовать для обмена с сервером структурированной информацией. Их форма – вложенные теги – хорошо подходит для хранения большинства данных, ну или по крайней мере лучше, чем текстовые файлы. Интерфейс DOM неуклюж в плане извлечения информации, и XML документы получаются довольно многословными. Обычно лучше общаться при помощи данных в формате JSON, которые проще читать и писать – как программам, так и людям.

var req = new XMLHttpRequest();

req.open(«GET», «example/fruit.json», false);

console.log(JSON.parse(req.responseText));

// → {banana: «yellow», lemon: «yellow», cherry: «red»}

HTTP-запросы из веб-страницы вызывают вопросы касаемо безопасности. Человек, контролирующий скрипт, может иметь интересы отличные от интересов пользователя, на чьём компьютере он запущен. Конкретно, если я зашёл на сайт themafia.org, я не хочу, чтобы их скрипты могли делать запросы к mybank.com, используя информацию моего браузера в качестве идентификатора, и давая команду отправить все мои деньги на какой-нибудь счёт мафии.

Вебсайты могут защитить себя от подобных атак, но для этого требуются определённые усилия, и многие сайты с этим не справляются. Из-за этого браузеры защищают их, запрещая скриптам делать запросы к другим доменам (именам вроде themafia.org и mybank.com).

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

Access-Control-Allow-Origin: *

В главе 10 в нашей реализации модульной системы AMD мы использовали гипотетическую функцию backgroundReadFile. Она принимала имя файла и функцию, и вызывала эту функцию после прочтения содержимого файла. Вот простая реализация этой функции:

function backgroundReadFile(url, callback) {

var req = new XMLHttpRequest();

req.open(«GET», url, true);

req.addEventListener(«load», function() {

callback(req.responseText);

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

Аргумент callback (обратный вызов) – термин, часто использующийся для описания подобных функций. Функция обратного вызова передаётся в другой код, чтобы он мог позвать нас обратно позже.

Несложно написать свою вспомогательную функцию HTTP, специально скроенную под вашу программу. Предыдущая делает только GET-запросы, и не даёт нам контроля над заголовками или телом запроса. Можно написать ещё один вариант для запроса POST, или более общий, поддерживающий разные запросы. Многие библиотеки JavaScript предлагают обёртки для XMLHttpRequest.

Основная проблема с приведённой обёрткой – обработка ошибок. Когда запрос возвращает код статуса, обозначающий ошибку (от 400 и выше), он ничего не делает. В некоторых случаях это нормально, но представьте, что мы поставили индикатор загрузки на странице, показывающий, что мы получаем информацию. Если запрос не удался, потому что сервер упал или соединение прервано, страница будет делать вид, что она чем-то занята. Пользователь подождёт немного, потом ему надоест и он решит, что сайт какой-то дурацкий.

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

Обработка ошибок в асинхронном коде ещё сложнее, чем в синхронном. Поскольку нам часто приходится отделять часть работы и размещать её в функции обратного вызова, область видимости блока try теряет смысл. В следующем коде исключение не будет поймано, потому что вызов backgroundReadFile возвращается сразу же. Затем управление уходит из блока try, и функция из него не будет вызвана.

backgroundReadFile(«example/data.txt», function(text) {

throw new Error(«That was unexpected»);

console.log(«Hello from the catch block»);

Чтобы обрабатывать неудачные запросы, придётся передавать дополнительную функцию в нашу обёртку, и вызывать её в случае проблем. Другой вариант – использовать соглашение, что если запрос не удался, то в функцию обратного вызова передаётся дополнительный аргумент с описанием проблемы. Пример:

function getURL(url, callback) {

var req = new XMLHttpRequest();

req.open(«GET», url, true);

req.addEventListener(«load», function() {

callback(req.responseText);

callback(null, new Error(«Request failed: « +

req.addEventListener(«error», function() {

callback(null, new Error(«Network error»));

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

Код, использующий getURL, должен проверять не возвращена ли ошибка, и обрабатывать её, если она есть.

getURL(«data/nonsense.txt», function(content, error) {

console.log(«Failed to fetch nonsense.txt: « + error);

console.log(«nonsense.txt: « + content);

С исключениями это не помогает. Когда мы совершаем последовательно несколько асинхронных действий, исключение в любой точке цепочки в любом случае (если только вы не обернули каждый обработчик в свой блок try/catch) вывалится на верхнем уровне и прервёт всю цепочку.

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

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

Интерфейс обещаний не особенно интуитивно понятный, но мощный. В этой главе мы лишь частично опишем его. Больше информации можно найти на

www.promisejs.org

Для создания объекта promises мы вызываем конструктор Promise, задавая ему функцию инициализации асинхронного действия. Конструктор вызывает эту функцию и передаёт ей два аргумента, которые сами также являются функциями. Первая должна вызываться в удачном случае, другая – в неудачном.

И вот наша обёртка для запросов GET, которая на этот раз возвращает обещание. Теперь мы просто назовём его get.

return new Promise(function(succeed, fail) {

var req = new XMLHttpRequest();

req.open(«GET», url, true);

req.addEventListener(«load», function() {

succeed(req.responseText);

fail(new Error(«Request failed: « + req.statusText));

req.addEventListener(«error», function() {

fail(new Error(«Network error»));

Заметьте, что интерфейс к самой функции упростился. Мы передаём ей URL, а она возвращает обещание. Оно работает как обработчик для выходных данных запроса. У него есть метод then, который вызывается с двумя функциями: одной для обработки успеха, другой – для неудачи.

get(«example/data.txt»).then(function(text) {

console.log(«data.txt: « + text);

console.log(«Failed to fetch data.txt: « + error);

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

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

Значит, вы можете использовать then для изменения результата обещания. К примеру, следующая функция возвращает обещание, чей результат – содержимое с данного URL, разобранное как JSON:

return get(url).then(JSON.parse);

Последний вызов then не обозначил обработчик неудач. Это допустимо. Ошибка будет передана в обещание, возвращаемое через then, а ведь это нам и надо – getJSON не знает, что делать, когда что-то идёт не так, но есть надежда, что вызывающий её код это знает.

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

Нам нужно получить имя матери супруга из example/bert.json. В случае проблем нам нужно убрать текст «загрузка» и показать сообщение об ошибке. Вот как это можно делать при помощи обещаний:

function showMessage(msg) {

var elt = document.createElement(«div»);

return document.body.appendChild(elt);

var loading = showMessage(«Загрузка…»);

getJSON(«example/bert.json»).then(function(bert) {

return getJSON(bert.spouse);

}).then(function(spouse) {

return getJSON(spouse.mother);

}).then(function(mother) {

showMessage(«Имя — « + mother.name);

}).catch(function(error) {

showMessage(String(error));

document.body.removeChild(loading);

Итоговая программа относительно компактна и читаема. Метод catch схож с then, но он ожидает только обработчик неудачного результата и в случае успеха передаёт дальше неизменённый результат. Исполнение программы будет продолжено обычным путём после отлова исключения – так же, как в случае с try/catch. Таким образом, последний then, удаляющий сообщение о загрузке, выполняется в любом случае, даже в случае неудачи.

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

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

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

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

Другой подход – построить свою систему общения на концепции ресурсов и методов HTTP. Вместо удалённого вызова процедуры по имени addUser вы делаете запрос PUT к /users/larry. Вместо кодирования свойств пользователя в аргументах функции вы определяете формат документа или используете существующий формат, который будет представлять пользователя. Тело PUT-запроса, создающего новый ресурс, будет просто документом этого формата. Ресурс получается через запрос GET к его URL (/user/larry), который возвращает представляющий этот ресурс документ.

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

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

Если нужно хранить что-либо в секрете, например пароли к емейлу, или данным необходимо прийти в пункт назначения в неизменном виде — таким, например, как номер банковского счёта, на который вы переводите деньги,- простого HTTP недостаточно.

Безопасный протокол HTTP, URL которого начинаются с https://, оборачивает HTTP-трафик так, чтобы его было сложнее прочитать и поменять. Сначала клиент подтверждает, что сервер – тот, за кого себя выдаёт, требуя с сервера представить криптографический сертификат, выданный авторитетной стороной, которую признаёт браузер. Потом, все данные, проходящие через соединение, шифруются так, чтобы предотвратить прослушку и изменение.

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

В этой главе мы увидели, что HTTP – это протокол доступа к ресурсам в интернете. Клиент отправляет запрос, содержащий метод (обычно GET), и путь, который определяет ресурс. Сервер решает, что ему делать с запросом и отвечает с кодом статуса и телом ответа. Запросы и ответы могут содержать заголовки, в которых передаётся дополнительная информация.

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

Интерфейс, через который JavaScript делает HTTP-запросы из браузера, называется XMLHttpRequest. Можно игнорировать приставку “XML” (но писать её всё равно нужно). Использовать его можно двумя способами: синхронным, который блокирует всю работу до окончания выполнения запроса, и асинхронным, который требует установки обработчика событий, отслеживающего окончание запроса. Почти во всех случаях предпочтительным является асинхронный способ. Создание запроса выглядит так:

var req = new XMLHttpRequest();

req.open(«GET», «example/data.txt», true);

req.addEventListener(«load», function() {

console.log(req.statusCode);

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

Согласование содержания (content negotiation)

Одна из вещей, которые HTTP умеет делать, но которую мы не обсуждали, называется согласованием содержания. Заголовок Accept в запросе можно использовать для сообщения серверу того, какие типы документов клиент желает получить. Многие серверы его игнорируют, но когда сервер знает о разных способах кодирования ресурса, он может взглянуть на заголовок и отправить тот, который предпочитает клиент.

URL eloquentjavascript.net/author настроен на ответ как прямым текстом, так и HTML или JSON, в зависимости от запроса клиента. Эти форматы определяются стандартизированными типами содержимого text/plain, text/html, и application/json.

Отправьте запрос для получения всех трёх форматов этого ресурса. Используйте метод setRequestHeader объекта XMLHttpRequest для установки заголовка Accept в один из нужных типов содержимого. Убедитесь, что вы устанавливаете заголовок после open, но перед send.

Наконец, попробуйте запросить содержимое типа application/rainbows+unicorns и посмотрите, что произойдёт.

Ожидание нескольких обещаний

У конструктора Promise есть метод all, который, получая массив обещаний, возвращает обещание, которое ждёт завершения всех указанных в массиве обещаний. Затем он выдаёт успешный результат и возвращает массив с результатами. Если какие-то из обещаний в массиве завершились неудачно, общее обещание также возвращает неудачу (со значением неудавшегося обещания из массива).

Попробуйте сделать что-либо подобное, написав функцию all.

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

return new Promise(function(success, fail) {

all([]).then(function(array) {

console.log(«Это должен быть []:», array);

return new Promise(function(success) {

setTimeout(function() { success(val); },

all([soon(1), soon(2), soon(3)]).then(function(array) {

console.log(«Это должен быть [1, 2, 3]:», array);

return new Promise(function(success, fail) {

fail(new Error(«бабах»));

all([soon(1), fail(), soon(3)]).then(function(array) {

console.log(«Сюда мы попасть не должны «);

if (error.message != «бабах»)

console.log(«Неожиданный облом:», error);

XMLHttpRequest

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

Несмотря на наличие слова «XML» в названии, XMLHttpRequest может работать с любыми данными, а не только с XML. Мы можем загружать/скачивать файлы, отслеживать прогресс и многое другое.

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

В современной веб-разработке XMLHttpRequest используется по трём причинам:

  1. По историческим причинам: существует много кода, использующего XMLHttpRequest, который нужно поддерживать.
  2. Необходимость поддерживать старые браузеры и нежелание использовать полифилы (например, чтобы уменьшить количество кода).
  3. Потребность в функциональности, которую fetch пока что не может предоставить, к примеру, отслеживание прогресса отправки на сервер.

Что-то из этого списка звучит знакомо? Если да, тогда вперёд, приятного знакомства с XMLHttpRequest. Если же нет, возможно, имеет смысл изучать сразу info:fetch.

Основы

XMLHttpRequest имеет два режима работы: синхронный и асинхронный.

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

Чтобы сделать запрос, нам нужно выполнить три шага:

  1. Создать XMLHttpRequest.

    let xhr = new XMLHttpRequest(); // у конструктора нет аргументов

    Конструктор не имеет аргументов.

  2. Инициализировать его.

    xhr.open(method, URL, [async, user, password])

    Этот метод обычно вызывается сразу после new XMLHttpRequest. В него передаются основные параметры запроса:

    • method — HTTP-метод. Обычно это "GET" или "POST".
    • URL — URL, куда отправляется запрос: строка, может быть и объект URL.
    • async — если указать false, тогда запрос будет выполнен синхронно, это мы рассмотрим чуть позже.
    • user, password — логин и пароль для базовой HTTP-авторизации (если требуется).

    Заметим, что вызов open, вопреки своему названию, не открывает соединение. Он лишь конфигурирует запрос, но непосредственно отсылается запрос только лишь после вызова send.

  3. Послать запрос.

    Этот метод устанавливает соединение и отсылает запрос к серверу. Необязательный параметр body содержит тело запроса.

    Некоторые типы запросов, такие как GET, не имеют тела. А некоторые, как, например, POST, используют body, чтобы отправлять данные на сервер. Мы позже увидим примеры.

  4. Слушать события на xhr, чтобы получить ответ.

    Три наиболее используемых события:

    • load — происходит, когда получен какой-либо ответ, включая ответы с HTTP-ошибкой, например 404.
    • error — когда запрос не может быть выполнен, например, нет соединения или невалидный URL.
    • progress — происходит периодически во время загрузки ответа, сообщает о прогрессе.
    xhr.onload = function() {
      alert(`Загружено: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // происходит, только когда запрос совсем не получилось выполнить
      alert(`Ошибка соединения`);
    };
    
    xhr.onprogress = function(event) { // запускается периодически
      // event.loaded - количество загруженных байт
      // event.lengthComputable = равно true, если сервер присылает заголовок Content-Length
      // event.total - количество байт всего (только если lengthComputable равно true)
      alert(`Загружено ${event.loaded} из ${event.total}`);
    };

Вот полный пример. Код ниже загружает /article/xmlhttprequest/example/load с сервера и сообщает о прогрессе:

// 1. Создаём новый XMLHttpRequest-объект
let xhr = new XMLHttpRequest();

// 2. Настраиваем его: GET-запрос по URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Отсылаем запрос
xhr.send();

// 4. Этот код сработает после того, как мы получим ответ сервера
xhr.onload = function() {
  if (xhr.status != 200) { // анализируем HTTP-статус ответа, если статус не 200, то произошла ошибка
    alert(`Ошибка ${xhr.status}: ${xhr.statusText}`); // Например, 404: Not Found
  } else { // если всё прошло гладко, выводим результат
    alert(`Готово, получили ${xhr.response.length} байт`); // response -- это ответ сервера
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Получено ${event.loaded} из ${event.total} байт`);
  } else {
    alert(`Получено ${event.loaded} байт`); // если в ответе нет заголовка Content-Length
  }

};

xhr.onerror = function() {
  alert("Запрос не удался");
};

После ответа сервера мы можем получить результат запроса в следующих свойствах xhr:

status
: Код состояния HTTP (число): 200, 404, 403 и так далее, может быть 0 в случае, если ошибка не связана с HTTP.

statusText
: Сообщение о состоянии ответа HTTP (строка): обычно OK для 200, Not Found для 404, Forbidden для 403, и так далее.

response (в старом коде может встречаться как responseText)
: Тело ответа сервера.

Мы можем также указать таймаут — промежуток времени, который мы готовы ждать ответ:

xhr.timeout = 10000; // таймаут указывается в миллисекундах, т.е. 10 секунд

Если запрос не успевает выполниться в установленное время, то он прерывается, и происходит событие timeout.

Чтобы добавить к URL параметры, вида `?name=value`, и корректно закодировать их, можно использовать объект [URL](info:url):

```js
let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// параметр 'q' закодирован
xhr.open('GET', url); // https://google.com/search?q=test+me%21
```

Тип ответа

Мы можем использовать свойство xhr.responseType, чтобы указать ожидаемый тип ответа:

  • "" (по умолчанию) — строка,
  • "text" — строка,
  • "arraybuffer"ArrayBuffer (для бинарных данных, смотрите в info:arraybuffer-binary-arrays),
  • "blob"Blob (для бинарных данных, смотрите в info:blob),
  • "document" — XML-документ (может использовать XPath и другие XML-методы),
  • "json" — JSON (парсится автоматически).

К примеру, давайте получим ответ в формате JSON:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

*!*
xhr.responseType = 'json';
*/!*

xhr.send();

// тело ответа {"сообщение": "Привет, мир!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Привет, мир!
};
В старом коде вы можете встретить свойства `xhr.responseText` и даже `xhr.responseXML`.

Они существуют по историческим причинам, раньше с их помощью получали строки или XML-документы. Сегодня следует устанавливать желаемый тип объекта в `xhr.responseType` и получать `xhr.response`, как показано выше.

Состояния запроса

У XMLHttpRequest есть состояния, которые меняются по мере выполнения запроса. Текущее состояние можно посмотреть в свойстве xhr.readyState.

Список всех состояний, указанных в спецификации:

UNSENT = 0; // исходное состояние
OPENED = 1; // вызван метод open
HEADERS_RECEIVED = 2; // получены заголовки ответа
LOADING = 3; // ответ в процессе передачи (данные частично получены)
DONE = 4; // запрос завершён

Состояния объекта XMLHttpRequest меняются в таком порядке: 0 -> 1 -> 2 -> 3 -> … -> 3 -> 4. Состояние 3 повторяется каждый раз, когда получена часть данных.

Изменения в состоянии объекта запроса генерируют событие readystatechange:

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // загрузка
  }
  if (xhr.readyState == 4) {
    // запрос завершён
  }
};

Вы можете наткнуться на обработчики события readystatechange в очень старом коде, так уж сложилось исторически, когда-то не было событий load и других. Сегодня из-за существования событий load/error/progress можно сказать, что событие readystatechange «морально устарело».

Отмена запроса

Если мы передумали делать запрос, можно отменить его вызовом xhr.abort():

xhr.abort(); // завершить запрос

При этом генерируется событие abort, а xhr.status устанавливается в 0.

Синхронные запросы

Если в методе open третий параметр async установлен на false, запрос выполняется синхронно.

Другими словами, выполнение JavaScript останавливается на send() и возобновляется после получения ответа. Так ведут себя, например, функции alert или prompt.

Вот переписанный пример с параметром async, равным false:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', *!*false*/!*);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Ошибка ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // для отлова ошибок используем конструкцию try...catch вместо onerror
  alert("Запрос не удался");
}

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

Многие продвинутые возможности XMLHttpRequest, такие как выполнение запроса на другой домен или установка таймаута, недоступны для синхронных запросов. Также, как вы могли заметить, ни о какой индикации прогресса речь тут не идёт.

Из-за всего этого синхронные запросы используют очень редко. Мы более не будем рассматривать их.

HTTP-заголовки

XMLHttpRequest умеет как указывать свои заголовки в запросе, так и читать присланные в ответ.

Для работы с HTTP-заголовками есть 3 метода:

setRequestHeader(name, value)
: Устанавливает заголовок запроса с именем name и значением value.

Например:

```js
xhr.setRequestHeader('Content-Type', 'application/json');
```

```warn header="Ограничения на заголовки"
Некоторые заголовки управляются исключительно браузером, например `Referer` или `Host`, а также ряд других.
Полный список [тут](https://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method).

`XMLHttpRequest` не разрешено изменять их ради безопасности пользователей и для обеспечения корректности HTTP-запроса.
```

````warn header="Поставленный заголовок нельзя снять"
Ещё одной особенностью `XMLHttpRequest` является то, что отменить `setRequestHeader` невозможно.

Если заголовок определён, то его нельзя снять. Повторные вызовы лишь добавляют информацию к заголовку, а не перезаписывают его.

Например:

```js
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// заголовок получится такой:
// X-Auth: 123, 456
```
````

getResponseHeader(name)
: Возвращает значение заголовка ответа name (кроме Set-Cookie и Set-Cookie2).

Например:

```js
xhr.getResponseHeader('Content-Type')
```

getAllResponseHeaders()
: Возвращает все заголовки ответа, кроме Set-Cookie и Set-Cookie2.

Заголовки возвращаются в виде единой строки, например:

```http
Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT
```

Между заголовками всегда стоит перевод строки в два символа `"rn"` (независимо от ОС), так что мы можем легко разделить их на отдельные заголовки. Значение заголовка всегда отделено двоеточием с пробелом `": "`. Этот формат задан стандартом.

Таким образом, если хочется получить объект с парами заголовок-значение, нам нужно задействовать немного JS.

Вот так (предполагается, что если два заголовка имеют одинаковое имя, то последний перезаписывает предыдущий):

```js
let headers = xhr
  .getAllResponseHeaders()
  .split('rn')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

// headers['Content-Type'] = 'image/png'
```

POST, FormData

Чтобы сделать POST-запрос, мы можем использовать встроенный объект FormData.

Синтаксис:

let formData = new FormData([form]); // создаём объект, по желанию берём данные формы <form>
formData.append(name, value); // добавляем поле

Мы создаём объект, при желании указываем, из какой формы form взять данные, затем, если нужно, с помощью метода append добавляем дополнительные поля, после чего:

  1. xhr.open('POST', ...) – создаём POST-запрос.
  2. xhr.send(formData) – отсылаем форму серверу.

Например:

<form name="person">
  <input name="name" value="Петя">
  <input name="surname" value="Васечкин">
</form>

<script>
  // заполним FormData данными из формы
  let formData = new FormData(document.forms.person);

  // добавим ещё одно поле
  formData.append("middle", "Иванович");

  // отправим данные
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

Обычно форма отсылается в кодировке multipart/form-data.

Если нам больше нравится формат JSON, то используем JSON.stringify и отправляем данные как строку.

Важно не забыть поставить соответствующий заголовок Content-Type: application/json, многие серверные фреймворки автоматически декодируют JSON при его наличии:

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "Вася",
  surname: "Петров"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

Метод .send(body) весьма всеяден. Он может отправить практически что угодно в body, включая объекты типа Blob и BufferSource.

Прогресс отправки

Событие progress срабатывает только на стадии загрузки ответа с сервера.

А именно: если мы отправляем что-то через POST-запрос, XMLHttpRequest сперва отправит наши данные (тело запроса) на сервер, а потом загрузит ответ сервера. И событие progress будет срабатывать только во время загрузки ответа.

Если мы отправляем что-то большое, то нас гораздо больше интересует прогресс отправки данных на сервер. Но xhr.onprogress тут не поможет.

Существует другой объект, без методов, только для отслеживания событий отправки: xhr.upload.

Он генерирует события, похожие на события xhr, но только во время отправки данных на сервер:

  • loadstart — начало загрузки данных.
  • progress — генерируется периодически во время отправки на сервер.
  • abort — загрузка прервана.
  • error — ошибка, не связанная с HTTP.
  • load — загрузка успешно завершена.
  • timeout — вышло время, отведённое на загрузку (при установленном свойстве timeout).
  • loadend — загрузка завершена, вне зависимости от того, как — успешно или нет.

Примеры обработчиков для этих событий:

xhr.upload.onprogress = function(event) {
  alert(`Отправлено ${event.loaded} из ${event.total} байт`);
};

xhr.upload.onload = function() {
  alert(`Данные успешно отправлены.`);
};

xhr.upload.onerror = function() {
  alert(`Произошла ошибка во время отправки: ${xhr.status}`);
};

Пример из реальной жизни: загрузка файла на сервер с индикацией прогресса:

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // отслеживаем процесс отправки
*!*
  xhr.upload.onprogress = function(event) {
    console.log(`Отправлено ${event.loaded} из ${event.total}`);
  };
*/!*

  // Ждём завершения: неважно, успешного или нет
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("Успех");
    } else {
      console.log("Ошибка " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

Запросы на другой источник

XMLHttpRequest может осуществлять запросы на другие сайты, используя ту же политику CORS, что и fetch.

Точно так же, как и при работе с fetch, по умолчанию на другой источник не отсылаются куки и заголовки HTTP-авторизации. Чтобы это изменить, установите xhr.withCredentials в true:

let xhr = new XMLHttpRequest();
*!*
xhr.withCredentials = true;
*/!*

xhr.open('POST', 'http://anywhere.com/request');
...

Детали по заголовкам, которые при этом необходимы, смотрите в главе fetch.

Итого

Типичный код GET-запроса с использованием XMLHttpRequest:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP ошибка?
    // обработаем ошибку
    alert( 'Ошибка: ' + xhr.status);
    return;
  }

  // получим ответ из xhr.response
};

xhr.onprogress = function(event) {
  // выведем прогресс
  alert(`Загружено ${event.loaded} из ${event.total}`);
};

xhr.onerror = function() {
  // обработаем ошибку, не связанную с HTTP (например, нет соединения)
};

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

  • loadstart — начало запроса.
  • progress — прибыла часть данных ответа, тело ответа полностью на данный момент можно получить из свойства responseText.
  • abort — запрос был прерван вызовом xhr.abort().
  • error — произошла ошибка соединения, например неправильное доменное имя. Событие не генерируется для HTTP-ошибок как, например, 404.
  • load — запрос успешно завершён.
  • timeout — запрос был отменён по причине истечения отведённого для него времени (происходит, только если был установлен таймаут).
  • loadend — срабатывает после load, error, timeout или abort.

События error, abort, timeout и load взаимно исключают друг друга — может произойти только одно из них.

Наиболее часто используют события завершения загрузки (load), ошибки загрузки (error), или мы можем использовать единый обработчик loadend для всего и смотреть в свойствах объекта запроса xhr детали произошедшего.

Также мы уже видели событие: readystatechange. Исторически оно появилось одним из первых, даже раньше, чем была составлена спецификация. Сегодня нет необходимости использовать его, так как оно может быть заменено современными событиями, но на него можно часто наткнуться в старом коде.

Если же нам нужно следить именно за процессом отправки данных на сервер, тогда можно использовать те же события, но для объекта xhr.upload.

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

Не пропустите эти материалы по теме:

  • Яндекс еда ошибка привязки карты
  • Xml код ошибки 503
  • Xmeye пользователь заблокирован код ошибки 11303
  • Xrdebugnew cpp как исправить ошибку
  • Xmeye ошибка имени пользователя или пароля

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии