среда, 11 марта 2020 г.

Ajax, XMLHttpRequest, GET, POST, Form, encodeURIComponent, application/x-www-form-urlencoded, multipart/form-data

Во время обычной отправки формы <form> браузер собирает значения из её полей, делает из них строку, которая вставляет в тело GET или POST запроса для отправки на сервер.

При отправке данных через XMLHttpRequest процедуру составления такой строки нужно делать самим в JavaScript-коде.

Кодирование запроса браузером - кодировка urlencoded.

urlencoded - это основной способ кодировки запросов, то есть стандартное кодирование URL.

Возьмем для примера следующую форму:

<form method="GET" action="/submit" >
  <input name="name" value="Boris">
  <input name="surname" value="Ivanov">
  <input type="submit" value="Send">
</form>

Здесь есть два поля: name=Boris и surname=Ivanov.

Браузер при составлении запроса перечисляет такие пары "имя=значение" через символ амперсанда "&" и, поскольку в нешей форме прописан метод GET, то итоговый запрос на сервер будет выглядеть как:

 /submit?name=Boris&surname=Ivanov

Все символы, кроме английских букв, цифр и символов - _ . ! ~ * ' ( ) заменяются на их цифровой код в UTF-8 со знаком %.

Например, пробел заменяется на %20, символ / на %2F, русские буквы кодируются двумя байтами в UTF-8, поэтому, к примеру, Ц заменится на %D0%A6.

Например, форма:

<form method="GET" action="/submit">
  <input name="name" value="Виктор">
  <input name="surname" value="Цой">
  <input type="submit" value="Send">
</form>

будет отправлена на сервер так:

/submit?name=%D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80&surname=%D0%A6%D0%BE%D0%B9

в JavaScript есть функция encodeURIComponent для кодирования символов "врчуную":

console.log(encodeURIComponent(' ')); // %20
console.log(encodeURIComponent('/')); // %2F
console.log(encodeURIComponent('В')); // %D0%92
console.log(encodeURIComponent('Виктор')); // %D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80

Эта кодировка используется в основном для метода GET, то есть для передачи параметра в строке запроса. По стандарту строка запроса не может содержать произвольные Unicode-символы, поэтому они кодируются как показано выше.

GET-запрос с urlencoded.

В методе GET параметры передаются в URL.

При формировании XMLHttpRequest, мы должны формировать запрос руками, кодируя поля из формы функцией encodeURIComponent.

Например, для посылки GET-запроса с параметрами name и surname, взятых из формы выше, их необходимо закодировать так:

var xhr = new XMLHttpRequest();

var params = 'name=' + encodeURIComponent(name) + '&surname=' + encodeURIComponent(surname);

xhr.open('GET', '/submit?' + params, true);

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

xhr.send();

Браузер автоматически добавит к данному запросу важнейшие HTTP-заголовки, такие как Content-Length и Connection.

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

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

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

xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

xhr.open(method, URL, [async, user, password]) - это метод, в который передаются основные параметры запроса:

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

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

Особенностью XMLHttpRequest является то, что отменить setRequestHeader() невозможно.

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

Например:

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

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

POST-запрос с urlencoded.

В методе POST параметры передаются не в URL, а в теле запроса. Оно указывается в вызове функции send(body).

В стандартных HTML-формах для метода POST доступны три кодировки, задаваемые через атрибут enctype:

application/x-www-form-urlencoded
multipart/form-data
text-plain

В зависимости от enctype браузер кодирует данные соответствующим способом перед отправкой на сервер.

При POST-запросе, созданном с помощью XMLHttpRequest обязательно надо задавать  заголовок Content-Type, содержащий одну из перечисленных выше кодировок.
Заданная кодировка указывает серверу как обрабатывать (раскодировать) пришедший запрос.

Для примера отправим запрос в кодировке application/x-www-form-urlencoded:

var body = 'name=' + encodeURIComponent(name) + '&surname=' + encodeURIComponent(surname);

var xhr = new XMLHttpRequest();

xhr.open('POST', '/submit', true);

xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

xhr.onreadystatechange = function () {
    ...
    ...
};

xhr.send(body);

В теле POST-запроса всегда используется только кодировка UTF-8, независимо от языка и кодировки страницы.

Если сервер вдруг ожидает данные в другой кодировке, к примеру windows-1251, то их нужно будет перекодировать.

POST-запрос с multipart/form-data.

Кодировка urlencoded за счёт замены символов на %-код может сильно раздуть общий объём пересылаемых данных.
Поэтому для пересылки файлов используется кодировка: multipart/form-data.

В этой кодировке поля пересылаются одно за другим через строку разделитель.

Чтобы использовать этот способ нужно указать метод POST и в атрибуте enctype multipart/form-data :
             
<form method="POST" action="/submit" enctype="multipart/form-data">
  <input name="name" value="Виктор">
  <input name="surname" value="Цой">
  <input type="submit" value="Send">
</form>

Форма при такой кодировке будет выглядеть примерно так:

... HTTP-Заголовки ...

Content-Type: multipart/form-data; boundary=RaNdOmDeLiMiTeR

--RaNdOmDeLiMiTeR
Content-Disposition: form-data; name="name"

Виктор
--RaNdOmDeLiMiTeR
Content-Disposition: form-data; name="surname"

Цой
--RaNdOmDeLiMiTeR--

То есть, поля передаются одно за другим, значения не кодируются, а чтобы было чётко понятно, какое значение где – поля разделены случайно сгенерированной строкой, которую называют "boundary" (англ. граница), в примере выше это RaNdOmDeLiMiTeR.

Сервер видит заголовок Content-Type: multipart/form-data, читает из него границу и раскодирует поля формы.

Такой способ используется в первую очередь при пересылке файлов, так перекодировка мегабайтов через urlencoded существенно загрузила бы браузер. Да и объём данных после неё сильно вырос бы.

Однако, никто не мешает использовать эту кодировку всегда для POST запросов.
Для GET доступна только urlencoded.

Сделать POST-запрос в кодировке multipart/form-data можно и через XMLHttpRequest.

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

Пример кода для того же запроса в кодировке multipart/form-data:

var data = {
  name: 'Виктор',
  surname: 'Цой'
};

var boundary = String(Math.random()).slice(2);
var boundaryMiddle = '--' + boundary + '\r\n';
var boundaryLast = '--' + boundary + '--\r\n'

var body = ['\r\n'];
for (var key in data) {
  // Добавление поля.
  body.push('Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + data[key] + '\r\n');
}

body = body.join(boundaryMiddle) + boundaryLast;

// Тело запроса готово, отправляем.

var xhr = new XMLHttpRequest();

xhr.open('POST', '/submit', true);

xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

xhr.onreadystatechange = function() {
  if (this.readyState != 4) return;
  console.log(this.responseText);
}

xhr.send(body);

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

Отправка на сервер файла через XMLHttpRequest.

Можно создать запрос, который сервер воспримет как загрузку файла.

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

Content-Disposition: form-data; name="myfile"; filename="pic.jpg"
Content-Type: image/jpeg
(пустая строка)
содержимое файла в виде строки

Пример:

var data = {
  myfile: 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUxLjEgKDU3NTAxKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5Hcm91cCBDb3B5IDI8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0iMl8xNDQwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzAuMDAwMDAwLCAtMTI3LjAwMDAwMCkiPgogICAgICAgICAgICA8ZyBpZD0iR3JvdXAtQ29weS0yIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMC4wMDAwMDAsIDEyNy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJGaWxsLTEiIGZpbGw9IiM5MkNGQUUiIHBvaW50cz0iMCAwIDAgMTguNDQ2NjAxOSAxOC40NDY2MDE5IDAiPjwvcG9seWdvbj4KICAgICAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJGaWxsLTIiIGZpbGw9IiMyQTgyODgiIHBvaW50cz0iMS41NTMzOTgwNiAyMCAyMCAyMCAyMCAxLjU1MzM5ODA2Ij48L3BvbHlnb24+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==',
};

var boundary = String(Math.random()).slice(2);
var boundaryMiddle = '--' + boundary + '\r\n';
var boundaryLast = '--' + boundary + '--\r\n'

var body = ['\r\n'];
for (var key in data) {
  // Добавление поля.
  body.push('Content-Disposition: form-data; name="' + key + '"\r\n\r\n' + data[key] + '\r\n');
}

body = body.join(boundaryMiddle) + boundaryLast;

// Тело запроса готово, отправляем.

var xhr = new XMLHttpRequest();

xhr.open('POST', '/submit', true);

xhr.setRequestHeader('Content-Disposition', 'form-data; name="myfile"; filename="pic.jpg"');
xhr.setRequestHeader('Content-Type', 'image/jpeg');

xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);

xhr.onreadystatechange = function() {
  if (this.readyState != 4) return;
  console.log(this.responseText);
}

xhr.send(body);

Формирование данных для отправки на сервер с помощью FormData.

Современные браузеры поддерживают встроенный объект FormData, который кодирует данные из формы для отправки на сервер.

Например:

<form name="person">
  <input name="name" value="Виктор">
  <input name="surname" value="Цой">
  <input type="submit" value="Send">
</form>

<script>

  // Создать объект для формы.
  var formData = new FormData(document.forms.person);

  // Добавить к пересылке ещё пару ключ - значение.
  formData.append("patronym", "Робертович");

  // Отправить запрос на сервер.
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "/url", false);
  xhr.send(formData);

</script>

Этот код отправит на сервер форму с полями name, surname и patronym.

Конструктор new FormData([form]) вызывается либо без аргументов, либо с DOM-элементом формы.

Метод formData.append(name, value) добавляет данные к форме.

Объект formData можно отсылать сразу, интеграция FormData с XMLHttpRequest встроена в браузер. Кодировка данных в запросе при этом будет multipart/form-data.

При просмотре содержимого объекта formData в консоли браузера на первый взгляд может показаться, что объект formData пустой. Но встроенный метод get() может выцепить данные value из него так:

var formData = new FormData(document.forms.person);
formData.get(document.forms.person.elements[0].name); // выдаст value первого дочернего input

Передача данных в формате JSON и другие кодировки запросов на сервер.

XMLHttpRequest сам по себе не ограничивает кодировку и формат пересылаемых данных.

Поэтому для обмена данными часто используется формат JSON:

var xhr = new XMLHttpRequest();

var body = JSON.stringify({
  name: "Виктор",
  surname: "Цой"
});

xhr.open('POST', '/submit', true)

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

xhr.onreadystatechange = function () {
    ...
    ...
};

// Отправляем объект в формате JSON с указанием Content-Type: application/json
// Сервер должен уметь такой Content-Type принять и раскодировать.

xhr.send(body);


Запросы на другой сервер.

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

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

var xhr = new XMLHttpRequest();

xhr.withCredentials = true;

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

Выводы.

У форм есть две основные кодировки:
application/x-www-form-urlencoded - по умолчанию и
multipart/form-data - для POST запросов, если явно указана в enctype.

Вторая кодировка обычно используется для больших данных и только для тела запроса.

Для составления запроса в application/x-www-form-urlencoded используется функция encodeURIComponent.

Для отправки запроса в multipart/form-data удобно использовать объект FormData.

Для обмена данными с сервером можно использовать формат JSON с указанием кодировки в заголовке Content-Type: application/json.

В XMLHttpRequest можно использовать и другие HTTP-методы, например PUT, DELETE, TRACE. К ним применимы все те же принципы, что описаны выше.

Комментариев нет:

Отправить комментарий