XMLHttpRequest
XMLHttpRequest — API-запит веб-клієнта (браузера) до веб-сервера за протоколом HTTP у фоновому режимі, для мов програмування JavaScript, JScript, VBScript і подібних. Використовується для синхронного або асинхронного обміну інформацією в довільному текстовому форматі (наприклад, XML, JSON, HTML). Дозволяє здійснювати HTTP-запити до віддаленого сервера без потреби перезавантажувати сторінку. Застосування XMLHttpRequest справляє враження «миттєвої» відповіді сервера, у порівнянні з класичними методом перезавантаження всієї сторінки для оновлення представленої на ній інформації.
XMLHttpRequest є невід'ємною частиною технології AJAX і використовується багатьма сайтами для створення динамічних веб-застосунків, що швидко реагують на запити користувача. Наприклад XMLHTTP використовується такими сайтами як Gmail, Google Suggest, MSN Virtual Earth та іншими. XMLHTTP працює лише з файлами, розташованими на тому ж домені, з якої завантажено сторінку. Як і у випадку JavaScript, це зроблено з метою забезпечення безпеки користувача (як захист від атаки, що має назву «міжсайтові сценарії», англ. cross-site scripting).
Історія
Вперше був реалізований компанією Microsoft, з'явившись в Internet Explorer 5.0 у вигляді об'єкта ActiveX, доступного через JavaScript, JScript, VBScript — скриптові мови, що підтримуються браузером. Програмісти проекту Mozilla потім розробили сумісну версію, під назвою XMLHttpRequest[1], в Mozilla 1.0. Надалі ця можливість також була реалізована компаніями Apple починаючи з Safari 1.2, спорідненим браузером Konqueror, компанією Opera Software починаючи з Opera 8.01, і ймовірно іншими.
Оскільки оригінальний XMLHttpRequest в IE5 та IE6 є об'єктом ActiveX, його неможливо розширити, додавши нові властивості і методи, що іноді є незручним обмеженням. Це обмеження було знято в реалізації Mozilla — XMLHttpRequest є повноцінним об'єктом JavaScript. Починаючи з IE7 Microsoft теж почав дотримуватися рекомендованого w3c визначення запиту.
Методи класу XMLHttpRequest
Метод | Опис |
---|---|
abort() | скасовує поточний запит |
getAllResponseHeaders() | повертає повний список HTTP-заголовків у вигляді рядка |
getResponseHeader (headerName) | повертає значення вказаного заголовка |
open (method, URL, async, userName, password) | визначає метод, URL і інші необов'язкові параметри запиту; параметр async визначає, чи відбувається робота в асинхронному режимі |
send (content) | відправляє запит на сервер |
setRequestHeader (label, value) | додає HTTP-заголовок до запиту |
overrideMimeType (mimeType) | дозволяє вказати MIME-тип документа, якщо сервер його не передав або передав неправильно. Увага: метод відсутній в Internet Explorer |
Властивості класу XMLHttpRequest
Властивість | Опис |
---|---|
onreadystatechange | обробник події, яка відбувається при кожній зміні стану об'єкта (необхідний для асинхронного режиму) |
readyState | повертає поточний стан об'єкта (0 — не ініціалізовано, 1 — відкрито, 2 — відправлення даних, 3 — отримання даних і 4 — дані завантажено) |
responseText | текст відповіді на запит |
responseXML | текст відповіді на запит в вигляді XML, котрий пізніше може бути розібраний методами DOM |
status | повертає HTTP-стан у вигляді числа (404 — «Not Found, Не найдено», 200 — «OK» тощо) |
statusText | повертає стан у вигляді рядка («Not Found», «OK» тощо) |
Приклад використання
План роботи з об'єктом XMLHttpRequest можна представити так:
- Створення об'єкта XMLHttpRequest
- Встановлення для нього обробника події onreadystatechange
- Відкриття з'єднання з вказівкою типу запиту, URL і інших параметрів.
- Безпосередньо відправлення запиту.
Створення екземпляра класу XMLHttpRequest
Перший пункт: створення екземпляра класу XMLHttpRequest. Конструкція створення об'єкта відрізняється в залежності від версії браузера: у IE 5 та IE 6 вона реалізована через ActiveXObject, а в решті браузерах (IE 7, Mozilla, Opera, Netscape і Safari) — як вбудований об'єкт типу XMLHttpRequest.
Отже, виклик для ранніх версій Internet Explorer:
var req = new ActiveXObject("Microsoft.XMLHTTP");
У ранніх версіях Internet Explorer (до IE7) рекомендується використовувати:
var req = new ActiveXObject("Msxml2.XMLHTTP");
і для решти:
var req = new XMLHttpRequest();
Тобто, для забезпечення кросс-браузерності нашого коду, потрібно лише перевіряти наявність об'єктів window.XMLHttpRequest і window.ActiveXObject, і застосовувати присутній. Як універсальне рішення пропонується використання наступної функції:
function createRequestObject()
{
if (window.XMLHttpRequest) {
try {
return new XMLHttpRequest();
} catch (e){}
} else if (window.ActiveXObject) {
try {
return new ActiveXObject('Msxml2.XMLHTTP');
} catch (e){}
try {
return new ActiveXObject('Microsoft.XMLHTTP');
} catch (e){}
}
return null;
}
Установлення обробника події
Наступним кроком є створення обробника подій і відкриття з'єднання. Ці виклики виглядають просто і однаково:
req.onreadystatechange = processReqChange;
req.open(<"GET"|"POST"|...>, <url>, <asyncFlag>);
Відкриття з'єднання і відправлення
Після визначення всіх параметрів запиту його залишається тільки відправити. Робиться це функцією send(). Якщо необхідно передати на сервер POST-дані, їх треба підставити як параметр для цієї функції. POST-дані повинні бути згорнуті в URL-закодований рядок (кодування UTF-8). Іншими словами цей рядок матиме вигляд, який ми звикли бачити в командному рядку браузера, при передачі даних командою GET. При відправленні запиту методом GET — для версії без ACTIVEX необхідно вказати параметр null, в решті випадків можна не указувати ніяких параметрів, але не буде помилкою, якщо для GET завжди буде вказаний параметр null:
req.send(null);
Після цього починає працювати згаданий вище обробник подій. Він — фактично основна частина нашої програми. У обробнику зазвичай відбувається перехоплення всіх можливих кодів стану запиту і виклик відповідних дій, а також перехоплення можливих помилок. Власне, ось приклад частини коду з цими двома функціями:
var req;
function loadXMLDoc(url)
{
req = null;
if (window.XMLHttpRequest) {
try {
req = new XMLHttpRequest();
} catch (e){}
} else if (window.ActiveXObject) {
try {
req = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e){
try {
req = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e){}
}
}
if (req) {
req.onreadystatechange = processReqChange;
req.open("GET", url, true);
req.send(null);
}
}
function processReqChange()
{
// Тільки в стані "complete"
if (req.readyState == 4) {
// для стану "OK"
if (req.status == 200) {
// Якщо 200 - робимо потрібні дії (404 - не знайдено)
} else {
alert("Не вдалось одержати дані:\n" +
req.statusText);
}
}
}
Підсумковий код
Отже, початковий код JavaScript-частини:
var req;
var reqTimeout;
function loadXMLDoc(url) {
req = null;
if (window.XMLHttpRequest) {
try {
req = new XMLHttpRequest();
} catch (e){}
} else if (window.ActiveXObject) {
try {
req = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e){
try {
req = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e){}
}
}
if (req) {
req.onreadystatechange = processReqChange;
req.open("GET", url, true);
req.send(null);
reqTimeout = setTimeout("req.abort();", 5000);
} else {
alert("Браузер не підтримує AJAX");
}
}
function processReqChange() {
document.form1.state.value = stat(req.readyState);
if (req.readyState == 4) {
clearTimeout(reqTimeout);
document.form1.statusnum.value = req.status;
document.form1.status.value = req.statusText;
// only if "OK"
if (req.status == 200) {
document.form1.response.value=req.responseText;
} else {
alert("Не вдалося отримати дані:\n" + req.statusText);
}
}
}
function stat(n)
{
switch (n) {
case 0:
return "не ініціалізовано";
break;
case 1:
return "Завантаження...";
break;
case 2:
return "Завантажено";
break;
case 3:
return "В процесі...";
break;
case 4:
return "Виконано";
break;
default:
return "Невідомий стан";
}
}
function requestdata(params)
{
loadXMLDoc('examples/httpreq.php'+params);
}
Тепер — HTML-форма:
<form name=form1>
<table width=100% style="font-size: 100%">
<tr><td width=30% valign=top>
Стан запиту
<td width=70%>
<input size=25 disabled type=text name=state value="">
<tr><td valign=top>Код стану
<td><input disabled size=2 type=text name=statusnum value="">
<input disabled size=19 type=text name=status value="">
<tr><td valign=top>Дані від сервера
<td><textarea rows=6 name=response></textarea>
<tr><td>Рядок GET-запиту<td>
<input type=text name=getparams value="?">
<input type=button onclick="requestdata(getparams.value);" value="GET">
</table>
</form>
І наостанок, 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);
header("Expires: -1");
echo "Hello world!\n\n";
if (isset($a))
{
for ($i=1; $i < 10000; $i++)
{
echo 'Це тестовий рядок. ';
if (($i % 1000) == 0) flush();
}
}
if (count($_GET) > 0)
{
echo "\n\nПередано GET'ом\n"; print_r($_GET);
}
?>
Кодування
Всі параметри GET/POST, що йдуть на сервер, окрім випадку multipart/form-data, кодуються по різному в різних браузерах. Зокрема, Firefox користується стандартним кодом URL, Opera вдається до кодування в UTF-8, IE7 передає кирилицю не кодуючи, як є. Тому треба бути уважним, інформація про спосіб кодування присутня в заголовках запиту. Наприклад, в PHP їх потрібно за потреби перекодувати функцією iconv. Єдино, можна бути певним, що латиниця не перекодовується в будь-якому випадку, і якщо є можливість залишитися в рамках латиниці, це позбавить програміста від додаткових клопотів.
Відповідь сервера браузер сприймає в тому кодуванні, яке вказане в заголовку відповіді Content-Type. Тобто, знову ж таки, в PHP, щоб браузер сприйняв відповідь в Windows-1251, потрібно послати заголовок типу:
header(Content-Type: text/plain; charset=windows-1251);
Або ж, це має зробити сервер.
Відомі проблеми
Проблема з кешуванням в Microsoft Internet Explorer
Internet Explorer кешує GET-запити. Ті автори, які незнайомі з кешуванням HTTP, сподіваються, що GET-запити не кешуються, або що кеш може бути обійдений, як у разі натиснення кнопки оновлення. У деяких ситуаціях уникнення кешування дійсно є помилкою. Одним з рішень є використання методу POST, який ніколи не кешується; проте він призначений для інших операцій. Іншим рішенням є використання методу запиту GET, що включає унікальний рядок запиту з кожним викликом, як показано на прикладі нижче.
req.open("GET", "xmlprovider.php?hash=" + Math.random());
або установки заголовка Expires на минулу дату у вашому скрипті, який генерує вміст XML. У PHP це буде так:
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // disable IE caching
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-cache, must-revalidate");
header("Pragma: no-cache");
У сервлетах Java це буде так:
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setDateHeader("Expires", 0);
Інакше можна примусити об'єкт XMLHttpRequest завжди витягати новий вміст, не використовуючи кеш.
req.open("GET", "xmlprovider.php");
req.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
req.send(null);
Важливо відмітити, що всі ці методики повинні використовуватися у разі, коли кешування заважає. В основному ж краще отримати переваги в швидкості при кешуванні, можливо комбінуючи зі спеціально вказаними датами модифікації або іншими доречними заголовками на сервері так, щоб максимально використовувати кешування без отримання неправильних результатів.
Повторне використання об'єкта XmlHttpRequest
В Internet Explorer, якщо open() викликаний після установки onreadystatechange, може бути проблема з повторним використанням цього XmlHttpRequest. Щоб використовувати наново XmlHttpRequest, спочатку викликайте метод open(), а потім — призначайте onreadystatechange. Це потрібно тому, що IE неявно очищає об'єкт XmlHttpRequest в методі open(), якщо його стан «completed».
Викликати abort() для перенаправлення запиту на іншій URL не потрібно, навіть якщо поточний запит ще не завершився.
Витоки пам'яті
В Internet Explorer об'єкт XmlHttpRequest належить середовищу DOM/COM, а Javascript-функція — середовищу Javascript. Виклик req.onreadystatechange = function() { … } неявний круговий зв'язок: req посилається на функцію через onreadystatechange, а функція, через область видимості — бачить (посилається на) req.
Неможливість виявити і обірвати такий зв'язок в багатьох (до IE 6,7 редакцій червня 2007?) версіях Internet Explorer приводить до того, що XmlHttpRequest разом з відповіддю сервера, функція-обробник, і все замикання міцно осідають в пам'яті до перезавантаження браузера. Щоб цього уникнути, ряд фреймворків (YUI, dojo…) взагалі не ставлять onreadystatechange, а натомість через setTimeout перевіряють його readyState кожні 10 мілісекунд. Це розриває кругову зв'язку req <-> onreadystatechange, і витік пам'яті не загрожує навіть в найбільш глючних браузерах.
Обмеження безпеки
Кросс-доменний XMLHttpRequest
Для обмеження XmlHttpRequest використовується філософія «Same Origin Policy» — «Правило одного джерела». Воно дуже просте — кожен сайт працює в своїй пісочниці. Запит можна робити тільки на адреси з тим же протоколом, доменом, портом, що і поточна сторінка. Тобто, із сторінки на адресі http://site.com не можна зробити XmlHttpRequest на адресу https://web.archive.org/web/20190617134849/http://www.site.com/, http://site.com:81%5Bнедоступне+посилання+з+червня+2019%5D або https://web.archive.org/web/20030621190843/http://www.othersite.com/.
Це створює проблему, якщо хочеться узяти вміст з іншого сайту. Як правило, в цьому випадку замість XmlHttpRequest використовуються інші засоби, наприклад, завантаження через динамічно створюваний тег <script>. Але, здебільшого, XmlHttpRequest є зручнішим.
Проксі
Найпростіший спосіб обійти це обмеження — проксування. Припустимо, ми хочемо зробити запит з http://site.com на https://web.archive.org/web/20150508130049/http://remote.com/get.html. Замість вказівки remote.com у методі open(), там ставиться URL виду http://site.com/proxy/remote.com/get.html%5Bнедоступне+посилання+з+червня+2019%5D, а сервер на site.com вже обробляє цей запит, як треба.
Якщо remote.com знаходиться на іншому сервері, то серверу site.com доведеться проксувати відвідувачеві як запит, так і відповідь. При цьому, зрозуміло, site.com не отримає куки remote.com, тому з цієї точки зору для користувача все безпечно.
Використання наддомену
Часто кросбраузерні запити — це спосіб обійти обмеження в 2 одночасних з'єднання до одного домену-порту. Спосіб використовувати два різних сервера в спілкуванні з відвідувачем. Крос-доменні запити між наддоменами https://web.archive.org/web/20110102121034/http://a.site.com/, http://b.site.com%5Bнедоступне+посилання+з+червня+2019%5D на http://site.com допустимі, через властивість document.domain, яке треба встановити в site.com
// на сторінці а.site.com
…
document.domain="site.com";
…
// все, тепер можу робити XmlHttpRequest на site.com
req.open("POST", "http://site.com/giveme.php")
Будь-які запити допустимі між сайтами, що знаходяться в довіреній (trusted) зоні Internet Explorer. Отже, внутрішній корпоративний портал може бути у всіх в цій зоні, і робити запити до будь-яких сайтів.
Ще один хитрий підхід називається XHRIframeProxy, і дозволяє робити XmlHttpRequest до будь-яких доменів за допомогою хитрого iframe-хака.
В плагінах Google Chrome
Пишучи аддон до браузера Google Chrome можна дозволити робити запити на довільні сервери, записавши їхні адреси в manifest.json
[2]
{
"name": "My extension",
...
"permissions": [
"http://www.google.com/"
],
...
}
Примітки
- Mozilla намагалася зберегти максимальну сумісність із оригіналом, були вилучені лише пропрієтарні назви Microsoft та ActiveX
- http://code.google.com/chrome/extensions/xhr.html
Посилання
- Об'єкт XMLHttpRequest — робочий нарис W3C
- яваскрипт.укр/XMLHttpRequest — про XMLHttpRequest українською мовою.
- Apple Safari 1.2
- Microsoft IXMLHTTPRequest
- Mozilla XML Extras
- Mozilla XMLHttpRequest object HowTo