AJAX renovated

This commit is contained in:
Ilya Kantor 2015-02-24 15:23:46 +03:00
parent 6414382e2e
commit 3a0d1ff2fe
80 changed files with 1327 additions and 1822 deletions

View file

@ -414,7 +414,7 @@ alert( d.toString() ); // вывод, похожий на 'Wed Jan 26 2011 16:40
<dd>То же самое, что `toString()`, но дата в зоне UTC.</dd>
</dl>
<dt>`toISOString()`</dt>
<dd>Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE&lt;9.
<dd>Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE8-.
```js
//+ run

View file

@ -102,7 +102,7 @@ sayHi.[[Scope]] = window
При запуске функции её объект переменных `LexicalEnvironment` получает ссылку на "внешнее лексическое окружение" со значением из `[[Scope]]`.
Если переменная не найдена в функции -- она будет искаться в снаружи.
Если переменная не найдена в функции -- она будет искаться снаружи.
Именно благодаря этой механике в примере выше `alert(userName)` выводит внешнюю переменную. На уровне кода это выглядит как поиск во внешней области видимости, вне функции.

View file

@ -1,97 +1,100 @@
# JSONP, получение данных через SCRIPT
# Протокол JSONP
Если создать узел `SCRIPT` со ссылкой на внешний исходник, то при добавлении в документ запустится процесс загрузки. В ответ сервер может прислать скрипт, содержащий нужные данные.
Если создать тег `<script src>`, то при добавлении в документ запустится процесс загрузки `src`. В ответ сервер может прислать скрипт, содержащий нужные данные.
Основным преимуществом такого способа коммуникации является кросс-браузерность и возможность обратиться к любому домену.
Таким образом можно запрашивать данные с любого сервера, в любом браузере, без каких-либо разрешений и дополнительных проверок.
Протокол JSONP -- это "надстройка" над таким способом коммуникации. Здесь мы рассмотрим его использование в деталях.
[cut]
## Запрос SCRIPT
## Запрос
Простейший пример запроса:
```js
function attachScript(src) {
function addScript(src) {
var elem = document.createElement("script");
elem.src = src;
document.getElementsByTagName('head')[0].appendChild(elem);
document.head.appendChild(elem);
}
attachScript('/user.php?id=123');
addScript('user?id=123');
```
Такой вызов добавит в `HEAD` документа тег:
```js
<script src="/user.php?id=123"></script>
<script src="/user?id=123"></script>
```
Браузер тут же обработает его: запросит `/user.php?id=123` с заданного URL и выполнит.
Браузер тут же обработает его: запросит `/user?id=123` с заданного URL и выполнит.
## Обработка ответа, JSONP
В примере выше рассмотрено создание запроса, но как получить ответ? Самый простой способ -- это присвоить его в переменную, т.е. сервер ответит как-то так:
В примере выше рассмотрено создание запроса, но как получить ответ? Допустим, сервер хочет прислать объект с данными.
Конечно, он может присвоить её в переменную, например так:
```js
var response = "....";
// ответ сервера
var user = {name: "Вася", age: 25 };
```
...А браузер по `script.onload` отловит окончание загрузки и получит значение `response`.
...А браузер по `script.onload` отловит окончание загрузки и прочитает значение `user`.
Но что, если одновременно делается несколько запросов? Получается, нужно присваивать в разные переменные.
Для большей гибкости при ответе используется протокол `JSONP`.
Протокол JSONP как раз и призван облегчить эту задачу.
**При этом название функции браузер может передать вместе с запросом.**
Он очень простой:
Запрос:
<ol>
<li>Вместе с запросом клиент в специальном, заранее оговорённом, параметре передаёт название функции.
Обычно такой параметр называется `callback`. Например :
```js
attachScript('/user.php?id=123&*!*callback=onUserData*/!*');
addScript('user?id=123&*!*callback=onUserData*/!*');
```
Cервер кодирует данные как JavaScript-объект и оборачивает их в функцию, название которой получает из параметра `callback`:
</li>
<li>Cервер кодирует данные в JSON и оборачивает их в функцию, название которой получает из параметра `callback`:
```js
// ответ сервера
onUserData({
name: "Вася",
age: 25,
isAdmin: false
age: 25
});
```
**Формат запроса, когда JSON заворачивается в функцию, называется JSONP ("JSON with Padding").**
[warn header="Аспекты безопасности"]
<ol>
<li>При кросс-доменном получении скрипта JavaScript не имеет доступа к его тексту.</li>
<li>Клиентский код должен доверять серверу при таком запросе. Ведь серверу ничего не стоит добавить в скрипт любые команды.</li>
</li>
</ol>
Это и называется JSONP ("JSON with Padding").
[warn header="Аспект безопасности"]
Клиентский код должен доверять серверу при таком запросе. Ведь серверу ничего не стоит добавить в скрипт любые команды.
[/warn]
## Реестр CallbackRegistry
В примере выше функция `onUserData` должна быть глобальной, иначе скрипт её не увидит.
В примере выше функция `onUserData` должна быть глобальной, ведь `<script src>` выполняется в глобальной области видимости.
Хотелось бы не загрязнять глобальное пространство имён и вызывать транспорт с локальной или даже анонимной функцией, как-то так:
Хотелось бы не загрязнять глобальное пространство имён, или по крайней мере свести загрязнение к минимуму.
```js
scriptRequest("/user.php?id=123", function(data) {
/* обработать данные */
});
```
**Чтобы решить эти задачи, для каждого запроса генерируется временная функция и записывается в глобальный объект ("реестр").**
Как правило, для этого создают один глобальный объект "реестр", который мы назовём `CallbackRegistry`. Далее для каждого запроса в нём генерируется временная функция.
Тег будет выглядеть так:
```html
<script src="/user.php?id=123&callback=*!*CallbackRegistry.func12345*/!*"></script>
<script src="user?id=123&callback=*!*CallbackRegistry.func12345*/!*"></script>
```
Сервер обернёт ответ в функцию `CallbackRegistry.func12345`, она вызывает нужный обработчик и очищает память, удаляя себя.
Далее мы посмотрим код всего этого, но перед этим -- важный момент! Нужно предусмотреть обработку ошибок.
Далее мы посмотрим более полный код всего этого, но перед этим -- важный момент! Нужно предусмотреть обработку ошибок.
## Обнаружение ошибок
@ -106,39 +109,23 @@ scriptRequest("/user.php?id=123", function(data) {
Чтобы отловить их все "одним махом", используем следующий алгоритм:
<ol>
<li>Создаётся `SCRIPT`.</li>
<li>На `SCRIPT` ставятся обработчики `onreadystatechange` (для старых IE) и `onload/onerror` (для остальных браузеров).</li>
<li>Создаётся `<script>`.</li>
<li>На `<script>` ставятся обработчики `onreadystatechange` (для старых IE) и `onload/onerror` (для остальных браузеров).</li>
<li>При загрузке скрипт выполняет функцию-коллбэк `CallbackRegistry...`. Пусть она при запуске ставит флажок "все ок". А мы в обработчиках проверим -- если флага нет, то функция не вызывалась -- стало быть, ошибка при загрузке или содержимое скрипта некорректно.</li>
</ol>
## Полный код
## Полный пример
Итак, код. Он совсем небольшой, а без комментариев был бы ещё меньше:
Итак, код функции, которая вызывается с `url` и коллбэками.
Он совсем небольшой, а без комментариев был бы ещё меньше:
```js
//+ src="scriptRequest.js"
//+ src="jsonp/scriptRequest.js"
```
Серверный скрипт отсылает ответ примерно так:
Пример использования:
```js
var url = require('url');
var params = url.parse(req.url, true).query;
var data = {
name: "Вася",
age: 30 // данные из базы
}
res.end(params.callback + '(' + JSON.stringify(data) + ')' );
```
### Демо
Пример "в действии" с тремя запросами -- правильным и двумя ошибочными:
```js
//+ run
function ok(data) {
alert("Загружен пользователь " + data.name);
}
@ -148,12 +135,15 @@ function fail(url) {
}
// Внимание! Ответы могут приходить в любой последовательности!
scriptRequest("/files/tutorial/ajax/jsonp/user?id=123", ok, fail); // Загружен
scriptRequest("user?id=123", ok, fail); // Загружен
scriptRequest("/badurl.js", ok, fail); // fail, 404
scriptRequest("/", ok, fail); // fail, 200 но некорректный скрипт
```
Функция `scriptRequest` в том или ином виде, иногда урезанном, реализована в различных фреймворках. В неё можно добавить более точное сообщение об ошибке, отправку данных и т.п.
Демо, по нажатию на кнопке запускаются запросы выше:
[codetabs src="jsonp" height=100]
## COMET
@ -161,6 +151,3 @@ scriptRequest("/", ok, fail); // fail, 200 но некорректный скр
COMET через `SCRIPT` реализуется при помощи длинных опросов, также как мы обсуждали в главе [](/xhr-longpoll).
То есть, создаётся тег `<script>`, браузер запрашивает скрипт у сервера и... Сервер оставляет соединение висеть, пока не появится, что сказать. Когда сервер хочет отправить сообщение -- он отвечает, используя формат JSONP. И, тут же, новый запрос...
[head]
<script src="/files/tutorial/ajax/jsonp/scriptRequest.js"></script>
[/head]

View file

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="scriptRequest.js"></script>
</head>
<body>
<script>
function ok(data) {
alert("Загружен пользователь " + data.id + ": " + data.name);
}
function fail(url) {
alert('Ошибка при запросе ' + url);
}
function go() {
// ответы могут приходить в любой последовательности!
scriptRequest("user?id=123", ok, fail); // Загружен
scriptRequest("/badurl.js", ok, fail); // fail, 404
scriptRequest("index.html", ok, fail); // fail, 200 но некорректный скрипт
}
</script>
<button onclick='go()'>Сделать запросы</button>
</body>
</html>

View file

@ -6,7 +6,7 @@ function scriptRequest(url, onSuccess, onError) {
var scriptOk = false; // флаг, что вызов прошел успешно
// сгенерировать имя JSONP-функции для запроса
var callbackName = 'f'+String(Math.random()).slice(2);
var callbackName = 'cb' + String(Math.random()).slice(-6);
// укажем это имя в URL запроса
url += ~url.indexOf('?') ? '&' : '?';

View file

@ -0,0 +1,38 @@
var http = require('http');
var url = require('url');
var static = require('node-static');
var file = new static.Server('.', { cache: 0 });
function accept(req, res) {
var urlParsed = url.parse(req.url, true);
if (urlParsed.pathname == '/user') {
var id = urlParsed.query.id;
var callback = urlParsed.query.callback;
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
var user = {
name: "Вася",
id: id
};
res.end(callback + '(' + JSON.stringify(user) + ')');
} else {
file.serve(req, res);
}
}
// ------ запустить сервер -------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}

View file

@ -1,6 +1,15 @@
# EventSource - события с сервера
# Server Side Events -- события с сервера
Сразу заметим, что на текущий момент этот способ поддерживают все современные браузеры, кроме IE.
Современный стандарт [Server-Sent Events](https://html.spec.whatwg.org/multipage/comms.html#the-eventsource-interface) позволяет браузеру создавать специальный объект `EventSource`, который сам обеспечивает соединение с сервером, делает пересоединение в случае обрыва и генерирует события при поступлении данных.
Он, по дизайну, может меньше, чем WebSocket'ы.
С другой стороны, Server Side Events проще в реализации, работают по обычному протоколу HTTP и сразу поддерживают ряд возможностей, которые для WebSocket ещё надо реализовать.
Поэтому в тех случаях, когда нужна преимущественно односторонняя передача данных от сервера к браузеру, они могут быть удачным выбором.
Современный стандарт [Server-Sent Events](http://dev.w3.org/html5/eventsource/) позволяет браузеру создавать специальный объект `EventSource`, который сам обеспечивает соединение с сервером, делает пересоединение в случае обрыва и генерирует события при поступлении данных.
[cut]
## Получение сообщений
@ -12,7 +21,7 @@ var eventSource = new EventSource("/events/subscribe");
eventSource.onmessage = function(e) {
console.log("Пришло сообщение: " + e.data);
}
};
```
Чтобы соединение успешно открылось, сервер должен ответить с заголовком `Content-Type: text/event-stream`, а затем оставить соединение висящим и писать в него сообщения в специальном формате:
@ -35,13 +44,15 @@ data: из двух строк
</li>
</ul>
Здесь все очень просто и удобно, кроме разделения сообщения при переводе строки. Но, если подумать -- это не так уж страшно: на практике сложные сообщения обычно передаются в формате JSON. А перевод строки в нём кодируется как `\n`. Соответственно, что данные будут пересылаться так:
Здесь все очень просто и удобно, кроме разделения сообщения при переводе строки. Но, если подумать -- это не так уж страшно: на практике сложные сообщения обычно передаются в формате JSON. А перевод строки в нём кодируется как `\n`.
Соответственно, многострочные данные будут пересылаться так:
```
data: {"user":"Вася","message":"Сообщение 3\n из двух строк"}
```
..То есть, строка `data:` будет одна.
...То есть, строка `data:` будет одна, и накаких проблем с разделением сообщения нет.
## Восстановление соединения
@ -49,7 +60,7 @@ data: {"user":"Вася","message":"Сообщение 3\n из двух стр
Это очень удобно, никакой другой транспорт не обладает такой встроенной способностью.
[smart header="А что, если сервер "чисто" закрыл соединение?"]
[smart header="Как серверу полностью закрыть соединение?"]
При любом закрытии соединения, в том числе если сервер ответит на запрос и закроет соединение сам -- браузер через короткое время повторит свой запрос.
Есть лишь два способа, которыми сервер может "отшить" надоедливый `EventSource`:
@ -145,7 +156,7 @@ eventSource.onmessage = function(e) {
};
```
### Своё имя события: event
## Своё имя события: event
По умолчанию на события срабатывает обработчик `onmessage`, но можно сделать и свои события. Для этого сервер должен указать перед событием его имя после `event:`.
@ -170,34 +181,22 @@ data: Вася
```js
eventSource.addEventListener('join', function(e) {
alert('Пришёл ' + e.data);
}
});
eventSource.addEventListener('message', function(e) {
alert('Сообщение ' + e.data);
}
});
eventSource.addEventListener('leave', function(e) {
alert('Ушёл ' + e.data);
}
});
```
На момент написания этой статьи (конец 2012) браузер Chrome не поддерживал свои события.
## Демо
В примере ниже сервер посылает в соединение числа от 1 до 5, а затем -- событие `bye` и закрывает соединение. Браузер автоматически откроет его заново.
В примере ниже сервер посылает в соединение числа от 1 до 3, а затем -- событие `bye` и закрывает соединение. Браузер автоматически откроет его заново.
```js
//+ src="browser.js"
```
```js
//+ src="index.js" hide="Открыть код для сервера"
```
[iframe src="eventsource" border="1" zip link height=200]
[codetabs src="eventsource"]
## Кросс-доменность
@ -209,21 +208,46 @@ var source = new EventSource("http://pupkin.ru/stream", { withCredentials: true
Второй аргумент сделан объектом с расчётом на будущее. Пока что никаких других свойств там не поддерживается, только `withCredentials`.
При обработки события у `event` также появится свойство `origin`, содержащее адрес источника, откуда пришёл ответ.
Сервер при этом получит заголовок `Origin` с доменом запроса и должен ответить с заголовком `Access-Control-Allow-Origin``Access-Control-Allow-Credentials`, если стоит `withCredentials`), в точности как в главе [](/xhr-crossdomain).
При кросс-доменных запросах у событий `event` также появится дополнительное свойство `origin`, содержащее адрес источника, откуда пришли данные. Его можно использовать для дополнительной проверки со стороны браузера:
```js
eventSource.addEventListener('message', function(e) {
if (e.origin != 'http://javascript.ru') return;
alert('Сообщение ' + e.data);
});
```
## Итого
Объект `EventSource` создаётся так:
Объект `EventSource` предназначен для передачи текстовых сообщений с сервера, используя обычный протокол HTTP.
Он предлагает не только передачу сообщений, но и встроенную поддержку важных вспомогательных функций:
<ul>
<li>События `event`.</li>
<li>Автоматическое пересоединение, с настраиваемой задержкой `retry`.</li>
<li>Проверка текущего состояния подключения по `readyState`.</li>
<li>Идентификаторы сообщений `id` для точного возобновления потока данных, последний полученный идентификатор передаётся в заголовке `Last-Event-ID`.</li>
<li>Кросс-доменность CORS.</li>
</ul>
Этот набор функций делает EventSource достойной альтернативой WebSocket, которые хоть и потенциально мощнее, но требуют реализации всех этих функций на клиенте и сервере, поверх протокола.
Поддержка -- все браузеры, кроме IE.
<ul>
<li>Синтаксис:
```js
var source = new EventSource(src); // src - адрес с любого домена
var source = new EventSource(src[, credentials]); // src - адрес с любого домена
```
Второй необязательный аргумент, если указан в виде `{ withCredentials: true }`, инициирует отправку Cookie и данных авторизации при кросс-доменных запросах.
Безопасность при кросс-доменных запросах обеспечивается аналогично `XMLHttpRequest`.
**Свойства объекта:**
</li>
<li>Свойства объекта:
<dl>
<dt>`readyState`</dt>
<dd>Текущее состояние соединения, одно из `EventSource.CLOSING (=0)`, `EventSource.OPEN (=1)` или `EventSource.CLOSED (=2)`.</dd>
@ -233,15 +257,15 @@ var source = new EventSource(src); // src - адрес с любого доме
<dd>Параметры, переданные при создании объекта. Менять их нельзя.</dd>
<dt>
</dl>
**Методы:**
</li>
<li>Методы:
<dl>
<dt>`close()`</dt>
<dd>Закрывает соединение.</dd>
</dl>
**События:**
</li>
<li>События:
<dl>
<dt>`onmessage`</dt>
@ -255,33 +279,17 @@ var source = new EventSource(src); // src - адрес с любого доме
Эти события можно ставить напрямую через свойство: `source.onmessage = ...`.
Если сервер присылает имя события в `event:`, то такие события нужно обрабатывать через `addEventListener`.
**Формат ответа сервера:**
</li>
<li>Формат ответа сервера:
Сервер присылает пустые строки, либо строки, начинающиеся с:
<ul>
<li>`date:` -- сообщение.</li>
<li>`date:` -- сообщение, несколько таких строк подряд склеиваются и образуют одно сообщение.</li>
<li>`id:` -- обновляет `lastEventId`.</li>
<li>`retry:` -- указывает паузу между пересоединениями, в миллисекундах. JavaScript не может указать это значение, только сервер.</li>
<li>`event:` -- имя события, должен быть перед `date:`.</li>
</ul>
Сообщения разделяются двойным переводом строки.
Основным преимуществом `EventSource` является поддержка пересоединения и событий "из коробки".
Поддержка -- все браузеры, кроме IE.
</li>
</ul>
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

@ -1,36 +0,0 @@
if (!window.EventSource) {
document.write('<b>В этом браузере нет поддержки EventSource.</b>');
}
var eventSource;
function start() { // при нажатии на Старт
eventSource = new EventSource('digits');
var log = document.getElementById('log');
eventSource.onopen = function(e) {
log.innerHTML += "Соединение открыто<br>";
};
eventSource.onerror = function(e) {
if (this.readyState == EventSource.CONNECTING) {
log.innerHTML += "Соединение порвалось, пересоединяемся...<br>";
} else {
log.innerHTML += "Ошибка, состояние: " + this.readyState;
}
};
eventSource.addEventListener('bye', function(e) {
log.innerHTML += "Bye<br>";
}, false);
eventSource.onmessage = function(e) {
log.innerHTML += e.data + '<br>';
};
}
function stop() { // при нажатии на Стоп
eventSource.close();
}

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
var eventSource;
function start() { // при нажатии на Старт
if (!window.EventSource) {
alert('В этом браузере нет поддержки EventSource.');
return;
}
eventSource = new EventSource('digits');
eventSource.onopen = function(e) {
log("Соединение открыто");
};
eventSource.onerror = function(e) {
if (this.readyState == EventSource.CONNECTING) {
log("Соединение порвалось, пересоединяемся...");
} else {
log("Ошибка, состояние: " + this.readyState);
}
};
eventSource.addEventListener('bye', function(e) {
log("Bye: " + e.data);
}, false);
eventSource.onmessage = function(e) {
console.log(e);
log(e.data);
};
}
function stop() { // при нажатии на Стоп
eventSource.close();
log("Соединение завершено");
}
function log(msg) {
logElem.innerHTML += msg + "<br>";
}
</script>
</head>
<body>
<button onclick="start()">Старт</button>
<button onclick="stop()">Стоп</button>
Нажмите "Старт" для начала.
<div id="logElem"></div>
</body>
</html>

View file

@ -6,7 +6,7 @@ var fileServer = new (require('node-static')).Server('.');
function onDigits(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache'
});
@ -16,13 +16,17 @@ function onDigits(req, res) {
write();
function write() {
res.write('data: ' + ++i + '\n\n');
if (i == 5) {
clearInterval(timer);
i++;
res.end('event: bye\ndata: до свидания');
if (i == 4) {
res.write('event: bye\ndata: до свидания\n\n');
clearInterval(timer);
res.end();
return;
}
res.write('data: ' + i + '\n\n');
}
}

View file

@ -1,36 +0,0 @@
if (!window.EventSource) {
document.write('<b>В этом браузере нет поддержки EventSource.</b>');
}
var eventSource;
function start() { // при нажатии на Старт
eventSource = new EventSource('digits');
var log = document.getElementById('log');
eventSource.onopen = function(e) {
log.innerHTML += "Соединение открыто<br>";
};
eventSource.onerror = function(e) {
if (this.readyState == EventSource.CONNECTING) {
log.innerHTML += "Соединение порвалось, пересоединяемся...<br>";
} else {
log.innerHTML += "Ошибка, состояние: " + this.readyState;
}
};
eventSource.addEventListener('bye', function(e) {
log.innerHTML += "Bye<br>";
}, false);
eventSource.onmessage = function(e) {
log.innerHTML += e.data + '<br>';
};
}
function stop() { // при нажатии на Стоп
eventSource.close();
}

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="browser.js"></script>
</head>
<body>
<button onclick="start()">Старт</button>
<button onclick="stop()">Стоп</button>
Нажмите "Старт" для начала.
<div id="log"></div>
</body>
</html>

View file

@ -1,50 +0,0 @@
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var fileServer = new (require('node-static')).Server('.');
function onDigits(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
});
var i=0;
var timer = setInterval(write, 1000);
write();
function write() {
res.write('data: ' + ++i + '\n\n');
if (i == 5) {
clearInterval(timer);
res.end('event: bye\ndata: до свидания');
}
}
}
function accept(req, res) {
if (req.url == '/digits') {
onDigits(req, res);
return;
}
// всё остальное -- статика
fileServer.serve(req, res);
}
// ----- запуск accept как сервера из консоли или как модуля ------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}

View file

@ -1,6 +1,8 @@
# IFRAME для AJAX-запросов
# IFRAME для AJAX и COMET
Транспорт `iframe` -- самый кросс-браузерный, это его основное преимущество. Он работает везде: от IE6 до Opera Mobile.
Эта глава посвящена `IFRAME` -- самому древнему и кросс-браузерному способу AJAX-запросов.
Сейчас он используется, разве что, для поддержки кросс-доменных запросов в IE7- и для COMET в IE9-.
Для общения с сервером создается невидимый `IFRAME`. В него отправляются данные, и в него же сервер пишет ответ.
@ -12,18 +14,18 @@
### Двуличность IFRAME: окно+документ
Что такое `IFRAME`? На этот вопрос у браузера два ответа
Что такое IFRAME? На этот вопрос у браузера два ответа
<ol>
<li>`IFRAME` -- это HTML-тег: <code>&lt;iframe&gt;</code> со стандартным набором свойств.
<li>IFRAME -- это HTML-тег: <code>&lt;iframe&gt;</code> со стандартным набором свойств.
<ul>
<li>Тег можно создавать в JavaScript</li>
<li>У тега есть стили, можно менять.</li>
<li>К тегу можно обратиться через `document.getElementById` и другие методы.</li>
</ul>
</li>
<li>`IFRAME` -- это окно браузера, вложенное в основное
<li>IFRAME -- это окно браузера, вложенное в основное
<ul>
<li>`IFRAME` -- такое же по функционалу окно браузера, как и основное, с адресом и т.п.</li>
<li>IFRAME -- такое же по функционалу окно браузера, как и основное, с адресом и т.п.</li>
<li>Если документ в `IFRAME` и внешнее окно находятся на разных доменах, то прямой вызов методов друг друга невозможен.</li>
<li>Ссылку на это окно можно получить через `window.frames['имя фрейма']`.</li>
</ul>
@ -32,7 +34,7 @@
Для достижения цели мы будем работать как с тегом, так и с окном. Они, конечно же, взаимосвязаны.
**В теге `IFRAME` свойство `contentWindow` хранит ссылку на окно.**
**В теге `<iframe>` свойство `contentWindow` хранит ссылку на окно.**
Окна также содержатся в коллекции `window.frames`.
@ -134,7 +136,7 @@ function createIframe(name, src, debug) {
</dl>
```js
// Например: postToIframe('/vote.php', {mark:5}, 'frame1')
// Например: postToIframe('/vote', {mark:5}, 'frame1')
function postToIframe(url, data, target){
var phonyForm = document.getElementById('phonyForm');
@ -153,7 +155,7 @@ function postToIframe(url, data, target){
// заполнить форму данными из объекта
var html = [];
for(var key in data){
var value = data[key].replace(/"/g, "&quot;");
var value = String(data[key]).replace(/"/g, "&quot;");
// в старых IE нельзя указать name после создания input
// поэтому используем innerHTML вместо DOM-методов
html.push("<input type='hidden' name=\""+key+"\" value=\""+value+"\">");
@ -184,7 +186,7 @@ function postToIframe(url, data, target){
</script>
```
..То есть, вызывает из основного окна функцию обработки (`window.name` в ифрейме -- его имя).</li>
...То есть, вызывает из основного окна функцию обработки (`window.name` в ифрейме -- его имя).</li>
<li>Дополнительно нужен обработчик `iframe.onload` -- он сработает и проверит, выполнилась ли функция `CallbackRegistry[window.name]`. Если нет, значит какая-то ошибка. Сервер при нормальном потоке выполнения всегда отвечает её вызовом.</li>
</ol>
@ -196,30 +198,194 @@ function postToIframe(url, data, target){
<li>`iframePost(url, data, onSuccess, onError)` -- для POST-запросов на `url`. Значением `data` должен быть объект `ключ:значение` для пересылаемых данных, он конвертируется в поля формы.</li>
</ul>
```js
//+ src="samedomain.js"
Пример в действии, возвращающий дату сервера при GET и разницу между датами клиента и сервера при POST:
[codetabs src="date"]
Прямой вызов функции внешнего окна из ифрейма отлично работает, потому что они с одного домена. Если с разных, то нужны дополнительные действия, например:
<ul>
<li>В IE8+ есть интерфейс [postMessage](https://developer.mozilla.org/en-US/docs/DOM/window.postMessage) для общения между окнами с разных доменов.</li>
<li>В любых, даже самых старых IE, можно обмениваться данными через `window.name`. Эта переменная хранит "имя" окна или фрейма, которое не меняется при перезагрузке страницы.
Поэтому если мы сделали `POST` в `<iframe>` на другой домен и он поставил `window.name = "Вася"`, а затем сделал редирект на основной домен, то эти данные станут доступны внешней странице.</li>
<li>Также в совсем старых IE можно обмениваться данными через хеш, то есть фрагмент URL после `#`. Его изменение доступно между ифреймами с разных доменов и не переводит к перезагрузке страницы. Таким образом они могут передавать данные друг другу. Есть готовые библиотеки, которые реализуют этот подход, например [Porthole](http://ternarylabs.github.io/porthole/).</li>
</ul>
## IFRAME для COMET
Бесконечный IFRAME -- самый старый способ организации COMET. Когда-то он был основой AJAX-приложений, а сейчас -- используется лишь в случаях, когда браузер не поддерживает современный стандарт WebSocket, то есть для IE9-.
Этот способ основан на том, что браузер читает страницу последовательно и обрабатывает все новые теги по мере того, как сервер их присылает.
Классическая реализация -- это когда клиент создает невидимый IFRAME, ведущий на служебный URL. Сервер, получив соединение на этот URL, не закрывает его, а
время от времени присылает блоки сообщений <code>&lt;script&gt;...javascript...&lt;/script&gt;</code>. Появившийся в IFRAME'е javascript тут же выполняется браузером, передавая информацию на основную страницу.
Таким образом, для передачи данных используется "бесконечный" ифрейм, через который сервер присылает все новые данные.
Схема работы:
<ol>
<li>Создаётся `<iframe src="COMET_URL">`, по адресу `COMET_URL` расположен сервер.</li>
<li>Сервер выдаёт начало ("шапку") документа и останавливается, оставляя соединение активным.</li>
<li>Когда сервер хочет что-то отправить -- он пишет в соединение <code>&lt;script&gt;parent.onMessage(сообщение)&lt;/script&gt;</code> Браузер тут же выполняет этот скрипт -- так сообщение приходит на клиент.</li>
<li>Ифрейм, в теории, грузится бесконечно. Его завершение означает обрыв канала связи. Его можно поймать по `iframe.onload` и заново открыть соединение (создать новый `iframe`).</li>
</ol>
Также ифрейм можно пересоздавать время от времени, для очистки памяти от старых сообщений.
<img src="comet.png">
Ифрейм при этом работает только на получение данных с сервера, как альтернатива [Server Sent Events](/server-sent-events). Для запросов используется обычный `XMLHttpRequest`.
## Обход проблем с IE
Такое использование ифреймов является хаком. Поэтому есть ряд проблем:
<ol>
<li>Показывается индикатор загрузки, "курсор-часики".</li>
<li>При POST в `<iframe>` раздаётся звук "клика".</li>
<li>Браузер буферизует начало страницы.</li>
</ol>
Мы должны эти проблемы решить, прежде всего, в IE, поскольку в других браузерах есть [WebSocket](/websockets) и [Server Sent Events](/server-sent-events) .
Проще всего решить последнюю -- IE не начинает обработку страницы, пока она не загрузится до определенного размера.
Поэтому в таком `IFRAME` первые несколько сообщений задержатся:
```html
<!DOCTYPE HTML>
<html>
<body>
<script>parent.onMessage("привет");</script>
<script>parent.onMessage("от сервера");</script>
...
```
Код с серверной стороны (важна лишь выделенная часть):
Решение -- забить начало ифрейма чем-нибудь, поставить, например, килобайт пробелов в начале:
```html
<!DOCTYPE HTML>
<html>
<body>
******* 1 килобайт пробелов, а потом уже сообщения ******
<script>parent.onMessage("привет");</script>
<script>parent.onMessage("от сервера");</script>
...
```
Для решения проблемы с индикацией загрузки и клика в мы можем использовать безопасный ActiveX-объект `htmlfile`. IE не требует разрешений на его создание. Фактически, это независимый HTML-документ.
Оказывается, если `iframe` создать в нём, то никакой анимации и звуков не будет.
Итак, схема:
<ol>
<li>Основное окно `main` создёт вспомогательный объект: `new ActiveXObject("htmlfile")`. Это HTML-документ со своим `window`, похоже на встроенный `iframe`.</li>
<li>В `htmlfile` записывается `iframe`.</li>
<li>Цепочка общения: основное окно - `htmlfile` - ифрейм.</li>
</ol>
### iframeActiveXGet
На самом деле всё еще проще, если посмотреть на код:
Метод `iframeActiveXGet` по существу идентичен обычному `iframeGet`, которое мы рассмотрели. Единственное отличие -- вместо `createIframe` используется особый метод `createActiveXFrame`:
```js
res.write(200, {'Cache-Control': 'no-cache'});
function iframeActiveXGet(url, onSuccess, onError) {
var data = JSON.stringify(data из POST или дата сервера);
var iframeOk = false;
var iframeName = Math.random();
*!*
res.end('<script>parent.CallbackRegistry[window.name]('+data+')</script>');
var iframe = createActiveXFrame(iframeName, url);
*/!*
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
}
```
Пример в действии, возвращающий дату сервера при GET и клиента при POST:
### createActiveXFrame
[iframe src="samedomain" border="1" link]
В этой функции творится вся IE-магия:
```js
function createActiveXFrame(name, src) {
// (1)
var htmlfile = window.htmlfile;
if (!htmlfile) {
htmlfile = window.htmlfile = new ActiveXObject("htmlfile");
htmlfile.open();
// (2)
htmlfile.write("<html><body></body></html>");
htmlfile.close();
// (3)
htmlfile.parentWindow.CallbackRegistry = CallbackRegistry;
}
// (4)
src = src || 'javascript:false';
htmlfile.body.insertAdjacentHTML('beforeEnd',
"<iframe name='"+name+"' src='"+src+"'></iframe>");
return htmlfile.body.lastChild;
}
```
<ol>
<li>Вспомогательный объект `htmlfile` будет один и он будет глобальным. Можно и спрятать переменную в замыкании. Смысл в том, что в один `htmlfile` можно записать много ифреймов, так что не будем множить сущности и занимать ими лишнюю память.</li>
<li>В `htmlfile` можно записать любой текст и, при необходимости, через `document.write('<script>...<\/script>)`. Здесь мы делаем пустой документ.</li>
<li>Когда загрузится `iframe`, он сделает вызов:
```html
<script>
parent.CallbackRegistry[window.name]( объект с данными );
</script>
```
Здесь `parent'ом` для `iframe'а` будет `htmlfile`, т.е. `CallbackRegistry` будет искаться среди переменных соответствующего ему окна, а вовсе не верхнего `window`.
Окно для `htmlfile` доступно как `htmlfile.parentWindow`, копируем в него ссылку на реестр коллбэков `CallbackRegistry`. Теперь ифрейм его найдёт.</li>
<li>Далее вставляем ифрейм в документ. В старых `IE` нельзя поменять `name` ифрейму через DOM, поэтому вставляем строкой через `insertAdjacentHTML`.</li>
</ol>
Пример в действии (только IE):
[codetabs src="date-activex"]
Запрос, который происходит, полностью незаметен.
Метод POST делается аналогично, только форму ужно добавлять не в основное окно, а в `htmlfile`, через вызов `htmlfile.appendChild`. В остальном -- всё так же, как и при обычной отправке через ифрейм.
Впрочем, для COMET нужен именно GET.
Можно и сочетать эти способы: если есть ActiveX: `if ("ActiveXObject" in window)` -- используем методы для IE, описанные выше, а иначе -- обычные методы.
Вот мини-приложение с сервером на Node.JS, непрерывно получающее текущее время с сервера через `<iframe>`, сочетающее эти подходы:
[codetabs src="date-comet"]
Ещё раз заметим, что обычно такое сочетание не нужно, так как если не IE9-, то можно использовать более современные средства для COMET.
## Итого
<ul>
<li>Iframe позволяет делать "AJAX"-запросы и хитро обходить кросс-доменные ограничения в IE7-. Обычно для этого используют либо `window.name` с редиректом, либо хеш с библиотекой типа [Porthole](https://github.com/ternarylabs/porthole).</li>
<li>В IE9- iframe можно использовать для COMET. В IE10 уже есть WebSocket.</li>
</ul>
Существует ряд уже готовых клиент-серверных библиотек, которые реализуют AJAX/COMET, в том числе и через iframe, мы рассмотрим их позже. Поэтому совсем не обязательно делать "с нуля". Хотя, как можно видеть из главы, это совсем несложно.
**Прямой вызов функции внешнего окна из ифрейма отлично работает, потому что они с одного домена.**
..Но есть и способы делать запросы на другой домен при помощи ифреймов. Мы рассмотрим их в следующей главе.
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

@ -6,23 +6,23 @@
<script>
function ok(result) {
alert('ok: ' + result);
alert('result: ' + result);
}
function fail() {
alert('fail');
}
function go() {
if (!window.ActiveXObject) {
if (!("ActiveXObject" in window)) {
alert("Только Internet Explorer");
return;
}
iframeActiveXGet("samedomain.php", ok, fail);
iframeActiveXGet("server", ok, fail);
}
</script>
<button onclick="go()">iframeActiveXGet</button>
<button onclick="go()">iframeActiveXGet server date</button>
</body>

View file

@ -0,0 +1,42 @@
var http = require('http');
var url = require('url');
var static = require('node-static');
var file = new static.Server('.', { cache: 0 });
var multiparty = require('multiparty');
function accept(req, res) {
var urlParsed = url.parse(req.url, true);
res.setHeader('Cache-Control', 'no-cache');
if (urlParsed.pathname == '/server') {
res.end(wrap(new Date()));
return;
} else if (urlParsed.pathname == '/diff') {
var form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
var diff = new Date() - fields.clientDate[0];
res.end(wrap(diff));
});
} else {
file.serve(req, res);
}
}
function wrap(data) {
return '<script>parent.CallbackRegistry[window.name](' + JSON.stringify(data) + ')</script>';
}
// ------ запустить сервер -------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}

View file

@ -22,7 +22,7 @@ var IframeComet = new function() {
self.onError("Unable to connect");
}
if (!window.WebSocket && window.ActiveXObject) { // old ie
if ("ActiveXObject" in window) { // IE
createActiveXFrame(url);
} else {
createIframe(url);

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<body>
<script src="iframeComet.js"></script>
<button onclick="go()">IframeComet.open("comet");</button>
<div id="showElem"></div>
<script>
IframeComet.onMessage = IframeComet.onError = show;
function show(msg) {
showElem.innerHTML = msg;
}
function go() {
IframeComet.open("comet");
}
</script>
</body>
</html>

View file

@ -0,0 +1,42 @@
var http = require('http');
var url = require('url');
var static = require('node-static');
var file = new static.Server('.', { cache: 0 });
function accept(req, res) {
if (req.url == '/comet') {
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
res.write('<!DOCTYPE HTML><html> \
<head><meta junk="'+new Array(2000).join('*')+'"/> \
<script> \
var i = parent.IframeComet; \
i.onConnected()</script> \
</head><body>');
setInterval(function() {
var now = new Date();
var timeStr = now.getHours()+':'+now.getMinutes()+':'+now.getSeconds();
res.write('<script>i.onMessage("'+timeStr+'")</script>');
}, 1000);
return;
} else {
file.serve(req, res);
}
}
// ------ запустить сервер -------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}

View file

@ -39,7 +39,7 @@ function postToIframe(url, data, target){
// заполнить форму данными из объекта
var html = [];
for(var key in data){
var value = data[key].replace(/"/g, "&quot;");
var value = String(data[key]).replace(/"/g, "&quot;");
html.push("<input type='hidden' name=\""+key+"\" value=\""+value+"\">");
}
phonyForm.innerHTML = html.join('');
@ -47,3 +47,50 @@ function postToIframe(url, data, target){
phonyForm.submit();
}
var CallbackRegistry = {}; // реестр
function iframeGet(url, onSuccess, onError) {
var iframeOk = false; // флаг успешного ответа сервера
var iframeName = Math.random(); // случайное имя для ифрейма
var iframe = createIframe(iframeName, url);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true; // сервер ответил успешно
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если сервер не ответил как надо - что-то не так
}
}
// аналогично iframeGet, но в postToIframe передаются данные data
function iframePost(url, data, onSuccess, onError) {
var iframeOk = false;
var iframeName = Math.random();
var iframe = createIframe(iframeName);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
postToIframe(url, data, iframeName);
}

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<body>
<script src="iframe.js"></script>
<script>
function ok(result) {
alert('result: ' + result);
}
function fail() {
alert('fail');
}
</script>
<button onclick="iframeGet('server', ok, fail)">iframeGet server</button>
<button onclick="iframePost('diff', {clientDate: +new Date}, ok, fail)">iframePost diff</button>
</body>
</html>

View file

@ -0,0 +1,42 @@
var http = require('http');
var url = require('url');
var static = require('node-static');
var file = new static.Server('.', { cache: 0 });
var multiparty = require('multiparty');
function accept(req, res) {
var urlParsed = url.parse(req.url, true);
res.setHeader('Cache-Control', 'no-cache');
if (urlParsed.pathname == '/server') {
res.end(wrap(new Date()));
return;
} else if (urlParsed.pathname == '/diff') {
var form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
var diff = new Date() - fields.clientDate[0];
res.end(wrap(diff));
});
} else {
file.serve(req, res);
}
}
function wrap(data) {
return '<script>parent.CallbackRegistry[window.name](' + JSON.stringify(data) + ')</script>';
}
// ------ запустить сервер -------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}

View file

@ -1,45 +0,0 @@
var CallbackRegistry = {}; // реестр
function iframeGet(url, onSuccess, onError) {
var iframeOk = false; // флаг успешного ответа сервера
var iframeName = Math.random(); // случайное имя для ифрейма
var iframe = createIframe(iframeName, url);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true; // сервер ответил успешно
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если сервер не ответил как надо - что-то не так
}
}
// аналогично iframeGet, но в postToIframe передаются данные data
function iframePost(url, data, onSuccess, onError) {
var iframeOk = false;
var iframeName = Math.random();
var iframe = createIframe(iframeName);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
postToIframe(url, data, iframeName);
}

View file

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<script src="iframe.js"></script>
<script src="samedomain.js"></script>
<script>
function ok(result) {
alert('ok: ' + result);
}
function fail() {
alert('fail');
}
</script>
<button onclick="iframeGet('samedomain.php', ok, fail)">iframeGet</button>
<button onclick="iframePost('samedomain.php', {data: 'Client date: '+new Date}, ok, fail)">iframePost</button>
</body>
</html>

View file

@ -1,45 +0,0 @@
var CallbackRegistry = {}; // реестр
function iframeGet(url, onSuccess, onError) {
var iframeOk = false; // флаг успешного ответа сервера
var iframeName = Math.random(); // случайное имя для ифрейма
var iframe = createIframe(iframeName, url);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true; // сервер ответил успешно
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если сервер не ответил как надо - что-то не так
}
}
// аналогично iframeGet, но в postToIframe передаются данные data
function iframePost(url, data, onSuccess, onError) {
var iframeOk = false;
var iframeName = Math.random();
var iframe = createIframe(iframeName);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
postToIframe(url, data, iframeName);
}

View file

@ -1,7 +0,0 @@
<?php
header('Cache-Control: no-cache');
$data = @json_encode($_POST['data'] ?: 'Server date: '.date('H:i:s'));
?>
<script>
parent.CallbackRegistry[window.name](<?=$data?>);
</script>

View file

@ -1,8 +0,0 @@
<?php
header('Cache-Control: no-cache');
$data = json_encode($_POST['data'] ?: 'Server date: '.date('H:i:s'));
sleep(2);
?>
<script>
parent.CallbackRegistry[window.name](<?=$data?>);
</script>

View file

@ -1,120 +0,0 @@
# IFRAME в IE: cкрытие в ActiveX "htmlfile"
Так как в IE6-9 современные транспорты не поддерживаются, то приходится использовать `iframe`. И здесь есть две проблемы:
<ul>
<li>При POST в `IFRAME` раздаётся звук "клика".</li>
<li>Пока идёт запрос в ифрейм, показываются часики и анимируется загрузка страницы.</li>
</ul>
И того и другого можно избежать, и сделать запрос полностью незаметным, если воспользоваться некоторыми специфическими возможностями Internet Explorer.
[cut]
В Internet Explorer есть безопасный ActiveX-объект `htmlfile`. IE не требует разрешений на его создание. Фактически, это независимый HTML-документ.
Оказывается, если `iframe` создать в нём, то никакой анимации и звуков не будет.
Итак, схема:
<ol>
<li>Основное окно `main` создёт вспомогательный объект: `new ActiveXObject("htmlfile")`. Это HTML-документ со своим `window`, похоже на встроенный `iframe`.</li>
<li>В `htmlfile` записывается `iframe`.</li>
<li>Цепочка общения: основное окно - `htmlfile` - ифрейм.</li>
</ol>
## iframeActiveXGet
На самом деле всё еще проще, если посмотреть на код:
Метод `iframeActiveXGet` по существу идентичен обычному `iframeGet`, которое мы рассмотрели. Единственное отличие -- вместо `createIframe` используется особый метод `createActiveXFrame`:
```js
function iframeActiveXGet(url, onSuccess, onError) {
var iframeOk = false;
var iframeName = Math.random();
*!*
var iframe = createActiveXFrame(iframeName, url);
*/!*
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
iframe.onload = function() {
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
}
```
## createActiveXFrame
В этой функции творится вся IE-магия:
```js
function createActiveXFrame(name, src) {
// (1)
var htmlfile = window.htmlfile;
if (!htmlfile) {
htmlfile = window.htmlfile = new ActiveXObject("htmlfile");
htmlfile.open();
// (2)
htmlfile.write("<html><body></body></html>");
htmlfile.close();
// (3)
htmlfile.parentWindow.CallbackRegistry = CallbackRegistry;
}
// (4)
src = src || 'javascript:false';
htmlfile.body.insertAdjacentHTML('beforeEnd',
"<iframe name='"+name+"' src='"+src+"'></iframe>");
return htmlfile.body.lastChild;
}
```
<ol>
<li>Вспомогательный объект `htmlfile` будет один и он будет глобальным. Можно и спрятать переменную в замыкании. Смысл в том, что в один `htmlfile` можно записать много ифреймов, так что не будем множить сущности и занимать ими лишнюю память.</li>
<li>В `htmlfile` можно записать любой текст и, при необходимости, через `document.write('<script>...<\/script>)`. Здесь мы делаем пустой документ.</li>
<li>Когда загрузится `iframe`, он сделает вызов:
```php
<script>
parent.CallbackRegistry[window.name](<?=$data?>);
</script>
```
Здесь `parent'ом` для `iframe'а` будет `htmlfile`, т.е. `CallbackRegistry` будет искаться среди переменных соответствующего ему окна, а вовсе не верхнего `window`.
Окно для `htmlfile` доступно как `htmlfile.parentWindow`, копируем в него ссылку на реестр коллбэков `CallbackRegistry`. Теперь ифрейм его найдёт.</li>
<li>Далее вставляем ифрейм в документ. В старых `IE` нельзя поменять `name` ифрейму через DOM, поэтому вставляем строкой через `insertAdjacentHTML`.</li>
</ol>
Пример в действии (только IE):
[iframe src="activex" border=1 link zip]
Запрос, который происходит, полностью незаметен. Мы ещё воспользуемся описанным способом для реализации COMET.
## POST через ActiveX
Ранее мы рассмотрели метод GET. Метод POST подразумевает отправку формы в ифрейм.
Для этого форму нужно добавить не во внешнее окно, а в `htmlfile`, через `htmlfile.appendChild`.
В остальном -- всё также, как и при обычной отправке через ифрейм.
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

@ -0,0 +1,140 @@
# Атака CSRF
Нельзя говорить про AJAX и не упомянуть про важнейшую деталь его реализации -- анти-CSRF.
[CSRF](http://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D0%BB%D0%BA%D0%B0_%D0%BC%D0%B5%D0%B6%D1%81%D0%B0%D0%B9%D1%82%D0%BE%D0%B2%D1%8B%D1%85_%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2) (Cross-Site Request Forgery, также XSRF) -- опаснейшая атака, которая приводит к тому, что хакер может выполнить на неподготовленном сайте массу различных действий от имени других, зарегистрированных посетителей.
Какие это действия -- отправка ли сообщений, перевод денег со счёта на счёт или смена паролей -- зависят от сайта, но в любом случае эта атака входит в образовательный минимум веб-разработчика.
[cut]
## Злая форма
"Классический" сценарий атаки таков:
<ul>
<li>Вася является залогиненным на сайт, допустим, `mail.com`. У него есть сессия в куках.</li>
<li>Вася попал на "злую страницу", например хакер пригласил его сделать это письмом или как-то иначе.</li>
<li>На злой странице находится форма такого вида:
```html
<form action="http://mail.com/send" method="POST">
<input type="hidden" name="message" value="Сообщение">
...
</form>
```
</li>
<li>При заходе на злую страницу JavaScript вызывает `form.submit`, отправляя таким образом форму на `mail.com`.</li>
<li>Сайт `mail.com` проверяет куки, видит, что посетитель авторизован и обрабатывает форму. В данном форма предполагает посылку сообщения.</li>
</ul>
Итог атаки -- Вася, зайдя на злую страницу, ненароком отправил письмо от своего имени. Содержимое письма сформировано хакером.
## Защита
В примере выше атака использовала слабое звено авторизации.
**Куки позволяют сайту `mail.com` проверить, что пришёл именно Вася, но ничего не говорят про данные, которые он отправляет.**
Иначе говоря, куки не гарантируют, что форму создал именно Вася. Они только удостоверяют личность, но не данные.
Типичный способ защиты сайтов -- это "секретный ключ" (`secret`), специальное значение, которое генерируется случайным образом и сохраняется в сессии посетителя. Его знает только сервер, посетителю мы его даже не будем показывать.
Затем на основе ключа генерируется "токен" (`token`). Токен делается так, чтобы с одной стороны он был отличен от ключа, в частности, может быть много токенов для одного ключа, с другой -- чтобы было легко проверить по токену, сгенерирован ли он на основе данного ключа или нет.
Для каждого токена нужно дополнительное случайное значение, которое называют "соль" `salt`.
Формула вычисления токена:
```
token = salt + ":" + MD5(salt + ":" + secret)
```
Например:
<ol>
<li>В сессии хранится `secret="abcdef"`, это значение создаётся один раз.</li>
<li>Для нового токена сгенерируем `salt`, например пусть `salt="1234"`.</li>
<li>`token = "1234" + ":" + MD5("1234" + ":" + "abcdef") = "1234:5ad02792a3285252e524ccadeeda3401"`.</li>
</ol>
Это значение -- с одной стороны, случайное, с другой -- имея такой `token`, мы можем взять его первую часть `1234` в качестве `salt` и, зная `secret`, проверить по формуле, верно ли он вычислен.
Не зная `secret`, невозможно сгенерировать token, который сервер воспримет как правильный.
Далее, токен добавляется в качестве скрытого поля к каждой форме, генерируемой на сервере.
То есть, "честная" форма для отсылки сообщений, созданная на `http://mail.com`, будет выглядеть так:
```html
<form action="http://mail.com/send" method="POST">
*!*
<input type="hidden" name="csrf" value="1234:5ad02792a3285252e524ccadeeda3401">
*/!*
<textarea name="message">
...
</textarea>
</form>
```
При её отправке сервер проверит поле `csrf`, удостоверится в правильности токена, и лишь после этого отошлёт сообщение.
"Злая страница" при всём желании не сможет сгенерировать подобную форму, так как не владеет `secret`, и токен будет неверным.
Такой токен также называют "подписью" формы, которая удостоверяет, что форма сгенерирована именно на сервере.
[smart header="Подпись с полями формы"]
Эта подпись говорит о том, что автор формы -- сервер, но ничего не гарантирует относительно её содержания.
Есть ситуации, когда мы хотим быть уверены, что некоторые из полей формы посетитель не изменил самовольно. Тогда мы можем включить в MD5 для формулы токена эти поля, например:
```
token = salt + ":" + MD5(salt + ":" + secret + ":" + fields.money)
```
При отправке формы сервер проверит подпись, подставив в неё известный ему `secret` и присланное значение `fields.money`. При несовпадении либо `secret` не тот (хакер), либо `fields.money` изменено.
[/smart]
## Токен и AJAX
Теперь перейдём к AJAX-запросам.
Что если посылка сообщений в нашем интерфейсе реализуется через XMLHttpRequest?
Как и в случае с формой, мы должны "подписать" запрос токеном, чтобы гарантировать, что его содержимое прислано на сервер именно интерфейсом сайта, а не "злой страницей".
Здесь возможны варианты, самый простой -- это дополнительная кука.
<ol>
<li>При авторизации сервер устанавливает куку с именем `CSRF-TOKEN`, и пишет в неё токен.</li>
<li>Код, осуществляющий XMLHttpRequest, получает куку и ставит заголовок `X-CSRF-TOKEN` с ней:
```js
var request = new XMLHttpRequest();
var csrfCookie = document.cookie.match(/CSRF-TOKEN=([\w-]+)/);
if (csrfCookie) {
request.setRequestHeader("X-CSRF-TOKEN", csrfCookie[1]);
}
```
</li>
<li>Сервер проверяет, есть ли заголовок и содержит ли он правильный токен.</li>
</ol>
Защита действует потому, что прочитать куку может только JavaScript с того же домена. "Злая страница" не сможет "переложить" куку в заголовок.
Если нужно сделать не XMLHttpRequest, а, к примеру, динамически сгенерировать форму из JavaScript -- она также подписывается аналогичным образом, скрытое поле или дополнительный URL-параметр генерируется по куке.
## Итого
<ul>
<li>CSRF-атака -- это когда "злая страница" отправляет форму или запрос на сайт, где посетитель, предположительно, залогинен.
Если сайт проверяет только куки, то он такую форму принимает. А делать это не следует, так как её сгенерировал злой хакер.</li>
<li>Для защиты от атаки формы, которые генерирует `mail.com`, подписываются специальным токеном. Можно не все формы, а только те, которые осуществляют действия от имени посетителя, то есть могут служить объектом атаки.</li>
<li>Для подписи XMLHttpRequest токен дополнительно записывается в куку. Тогда JavaScript с домена `mail.com` сможет прочитать её и добавить в заголовок, а сервер -- проверить, что заголовок есть и содержит корректный токен.</li>
<li>Динамически сгенерированные формы подписываются аналогично: токен из куки добавляется как URL-параметр или дополнительное поле.</li>
</ul>
Чтобы

View file

@ -1,199 +0,0 @@
# IFRAME для кросс-доменных запросов
Современные браузеры поддерживают множество альтернатив ифреймам. В первую очередь, это современный [XMLHttpRequest](/ajax-xmlhttprequest), который может делать любые запросы, в том числе кросс-доменные и с пересылкой файлов.
А в IE6-9 ифреймы в некоторых ситуациях незаменимы, поэтому способ, описанный в этой статье, в первую очередь используется для поддержки этих браузеров.
Существует несколько вариантов реализации кросс-доменных запросов. Конкретный выбор зависит от версии IE, которую мы собираемся поддерживать.
## IE8+: postMessage-ответ
IE8 и выше поддерживают интерфейс [postMessage](https://developer.mozilla.org/en-US/docs/DOM/window.postMessage) для общения между окнами с разных доменов.
Для его использования серверный код должен ответить на запрос примерно так:
```js
<script>
parent.postMessage(message, parentDomain);
</script>
```
Браузер проверит, действительно ли родительское окно `parent` с домена `parentDomain`, и если это так, то на нём будет вызвано событие `onmessage`, которое получит доступ к тексту `message`.
В `message` должна содержаться информация о том, на какой запрос пришёл ответ, и данные. Так как кросс-браузерно поддерживаются только сообщения-строки, то передать эту информацию можно через JSON.
Серверный код, который это делает:
```php
<?php
header('Cache-Control: no-cache');
$data = json_encode($_POST['data'] ?: 'Server date: '.date('H:i:s'));
?>
<script>
*!*
parent.postMessage(JSON.stringify({
name: window.name,
body: <?=$data?>
}), "http://learn.javascript.ru");
*/!*
</script>
```
Код на клиенте, который будет реагировать на `postMessage`:
```js
window.onmessage = function(event) {
// распаковать сообщение
var message = JSON.parse(event.data);
// вызвать обработку результата запроса
CallbackRegistry[message.name](message.body);
}
```
Ещё одна небольшая тонкость -- время вызова `iframe.onload`.
Задача этого обработчика -- проверить, всё ли вызвалось. Для этого он должен отработать обязательно *после* `onmessage`. А вызов `postMessage` -- асинхронный, поэтому нужно отложить проверку на `setTimeout(..,0)`.
При этом получится следующий поток выполнения:
<ol>
<li>Пришёл ответ с сервера, вызвал `postMessage`.</li>
<li>Событие `onmessage` встало в очередь выполнения.</li>
<li>Обработано окончание загрузки, вызвался `iframe.onload`, поставил в очередь проверку.</li>
<li>Сработает `onmessage`, а затем проверка.</li>
</ol>
Полный код кросс-доменного `iframeGet` (`iframePost` -- аналогично):
```js
//+ src="postmessage.js"
```
Пример в действии:
[iframe src="postmessage" border="1" link zip]
### postMessage-прокси
Если нужно часто обмениваться данными с одним доменом, то можно загрузить `iframe` с этого домена, содержащий код `postMessage` и `onmessage`:
```html
<script>
window.onmessage = function(event) {
// получил информацию с основной страницы
if (event.origin != 'javascript.ru') return; // обратились с чужого сайта
// послать event.data на свой домен
// по окончании запроса вызвать parent.postMessage(ответ, ...)
}
</script>
```
Время от времени этому `iframe` пересылаются сообщения, он обменивается информацией со своей страницей (можно через `XMLHttpRequest`) и отвечает основной странице.
При этом ифрейм будет только один, пересоздавать ничего не нужно.
## IE6: передача через window.name
Этот способ активно используется, так как с одной стороны покрывает ряд важных сценариев использования, а с другой -- поддерживается всеми браузерами, включая IE6+.
У `IFRAME` есть атрибут `name`, который задаёт "имя окна". Обычно он используется для отправки форм в данный ифрейм или для доступа к внутреннему окну через `window.frames[name]`.
Но можно и использовать его для другой, непредусмотренной цели -- передачи данных.
Дело в том, что имя окна не меняется при навигации в `IFRAME`, и при этом доступно из JavaScript. Поэтому если одна страница поставила `window.name = "Вася"`, а потом посетитель нажал на ссылку в `IFRAME` и перешёл на другую страницу (домен не важен), то она сможет прочитать "Вася" из `window.name`.
Это делает возможным следующий алгоритм:
<ol>
<li>Страница на основном домене создаёт `IFRAME` и отправляет в него запрос *на другой домен*.</li>
<li>Сервер отвечает кодом, который выглядит примерно так:
```php
<?php ... ?>
<script>
window.name = "данные";
// IFRAME сам должен поменять свой адрес, иначе в IE name не сохранится
// blank.html - любой пустой документ на основном домене
window.location.replace('http://основной домен/blank.html');
</script>
```
</li>
<li>Страница по `iframe.onload` читает `iframe.contentWindow.name` и получает данные.</li>
</ol>
Ниже реализован метод POST, т.к. кросс-доменный GET можно сделать и при помощи [Транспорта SCRIPT](/ajax-jsonp):
```js
//+ src="name.js"
```
Обратите внимание на комментарии, они более подробно описывают важные детали коммуникации.
Пример в действии:
[iframe src="name" border="1" zip link]
### Сфера использования
У метода `window.name` почти нет недостатков.
<ul>
<li>Он работает во всех браузерах.</li>
<li>Он прост в реализации.</li>
<li>В `window.name` кросс-браузерно влезает до 2МБ.</li>
<li>Пустой документ `blank.html` будет сразу же закэширован браузером, и в дальнейшем лишнего запроса не будет.</li>
</ul>
Единственное ограничение -- ответ до 2МБ. Многие сценарии использования, например кросс-доменная авторизация, легко вписываются в них.
## IE6+: hash-прокси
Этот способ представляет собой отвратительнейший хак. Он заключается в том, что при общении между основной страницей и ифреймом с другого домена -- они могут легко менять `hash` друг другу (часть пути после `#`).
Считается, что это безопасно, т.к. изменение хэша -- всего лишь прыжок по странице, поэтому браузеры это разрешают.
При этом читать хэши друг друга нельзя, можно только писать в них.
Соответственно, логика взаимодействия такая:
<ol>
<li>Главная страница подключает ифрейм с другого домена.</li>
<li>Ифрейму меняется `hash`, он видит это и понимает, что ему "хотят что-то этим сказать", обрабатывает полученное сообщение.</li>
<li>Когда ифрейм готов, он, в свою очередь, меняет хэш родителю. Тот принимает ответ.</li>
</ol>
При этом у родителя и ифрейма существует протокол общения через хэш. Ограничение -- не более 4kb (минус длина базовой части URL) на сообщение.
Обход проблем:
<ul>
<li>**При такой реализации на п.3 окно-родитель будет "прыгать" -- ему же меняют хэш!**
Поэтому в реальности используют три ифрейма: родительское окно `main` включает ифрейм с того же домена `localProxy.html`, а уже он включает в себя ифрейм с другого домена `remoteProxy.html`.
Общение осуществляется по цепочке `main -> localProxy -> remoteProxy -> localProxy -> main`. Хэш меняется у промежуточного прокси, который сообщает об этом родительскому окну (он на одном домене с ним).
Можно сделать фреймы `localProxy` и `remoteProxy` на одном уровне. Тогда не во всех браузерах будет работать (в Opera не будет?), но в IE всё (вроде!) будет ок.</li
<li>**Как окну(или ифрейму) обнаружить изменение хэща?**
В старых браузерах отсутствует событие `onhashchange`, поэтому используются два подхода:
<ol>
<li>Окно раз в 50мс проверяет, поменялся ли хэш.</li>
<li>Используется `window.onresize` как сигнал: когда ифрейму меняем хэш -- тут же немного изменим и его размер, на нём сработает событие `onresize`.</li>
</ol>
</li>
</ul>
Автор этих строк реализовывал такой протокол в очень бородатые времена, несколько раз участвовал в обсуждениях проектов, использующих такое решение -- и всегда был удивлён, насколько стабильно оно работает. Несмотря на хакерскую природу самого решения.
В этом случае ифрейм перегружать не надо, поэтому большей частью такой подход используется даже не для AJAX, а для общения между постоянно встроенным виджетом-ифреймом и основной страницей.
Область применения -- когда обязательна поддержка IE6,7 и никакие другие способы, почему-то, не подходят.
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

@ -1,36 +0,0 @@
function iframePost(url, data, onSuccess, onError) {
var iframeName = Math.random();
var iframe = createIframe(iframeName);
// onload после append, чтобы не срабатывал лишний раз
iframe.onload = function() {
var newName = iframe.contentWindow.name;
if (newName == iframeName) {
// имя получили, но оно осталось тем же - значит сервер его не заменил
// значит что-то не так в ответе (ошибка сервера или неверный формат)
onError();
return;
}
// имя получено, коммуникация завершена
iframe.parentNode.removeChild(iframe); // очистка
// такой eval можно использовать вместо JSON.parse для старых IE,
// при условии доверия содержимому к newName
// если доверия нет -- используйте библиотеку json2.js
onSuccess( eval('(' + newName +')') );
};
// для IE8- нужно использовать attachEvent вместо iframe.onload
if (iframe.attachEvent && !iframe.addEventListener) {
iframe.attachEvent("onload", iframe.onload);
iframe.onload = null;
}
postToIframe(url, data, iframeName);
}

View file

@ -1,13 +0,0 @@
<?php
header('Cache-Control: no-cache');
$data = json_encode($_POST['data'] ?: 'Server date: '.date('H:i:s'));
// $blankUrl can be any empty document
$blankUrl = 'http://learn.javascript.ru'.dirname($_SERVER['REQUEST_URI']).'/blank.html';
?>
<script>
window.name = "<?=str_replace('"','\\"',$data)?>"; // положить JSON внутрь строки, иначе JSON.parse(window.name) не сработает
// сам IFRAME должен поменять свой адрес, иначе в IE name не сохранится
window.location.replace('<?=$blankUrl?>');
</script>

View file

@ -1,2 +0,0 @@
<!DOCTYPE HTML>
<html></html>

View file

@ -1,49 +0,0 @@
function createIframe(name, src, debug) {
src = src || 'javascript:false'; // пустой src
var tmpElem = document.createElement('div');
// в старых IE нельзя присвоить name после создания iframe, поэтому создаём через innerHTML
tmpElem.innerHTML = '<iframe name="'+name+'" id="'+name+'" src="'+src+'">';
var iframe = tmpElem.firstChild;
if (!debug) {
iframe.style.display = 'none';
}
document.body.appendChild(iframe);
return iframe;
}
// функция постит объект-хэш content в виде формы с нужным url , target
// напр. postToIframe('/count.php', {a:5,b:6}, 'frame1')
function postToIframe(url, data, target){
var phonyForm = document.getElementById('phonyForm');
if(!phonyForm){
// временную форму создаем, если нет
phonyForm = document.createElement("form");
phonyForm.id = 'phonyForm';
phonyForm.style.display = "none";
phonyForm.method = "POST";
phonyForm.enctype = "multipart/form-data";
document.body.appendChild(phonyForm);
}
phonyForm.action = url;
phonyForm.target = target;
// заполнить форму данными из объекта
var html = [];
for(var key in data){
var value = data[key].replace(/"/g, "&quot;");
html.push("<input type='hidden' name=\""+key+"\" value=\""+value+"\">");
}
phonyForm.innerHTML = html.join('');
phonyForm.submit();
}

View file

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<script src="iframe.js"></script>
<script src="name.js"></script>
<script>
function ok(result) {
alert('ok: ' + result);
}
function fail() {
alert('fail');
}
// запрос к файлу anydomain.php на другом домене
var url = 'http://test.javascript.ru' + location.pathname.replace(/[^\/]*$/, 'anydomain.php');
</script>
<button onclick="iframePost(url, {data: 'Client date: '+new Date}, ok, fail)">iframePost</button>
</body>
</html>

View file

@ -1,36 +0,0 @@
function iframePost(url, data, onSuccess, onError) {
var iframeName = Math.random();
var iframe = createIframe(iframeName);
// onload после append, чтобы не срабатывал лишний раз
iframe.onload = function() {
var newName = iframe.contentWindow.name;
if (newName == iframeName) {
// имя получили, но оно осталось тем же - значит сервер его не заменил
// значит что-то не так в ответе (ошибка сервера или неверный формат)
onError();
return;
}
// имя получено, коммуникация завершена
iframe.parentNode.removeChild(iframe); // очистка
// такой eval можно использовать вместо JSON.parse для старых IE,
// при условии доверия содержимому к newName
// если доверия нет -- используйте библиотеку json2.js
onSuccess( eval('(' + newName +')') );
};
// для IE8- нужно использовать attachEvent вместо iframe.onload
if (iframe.attachEvent && !iframe.addEventListener) {
iframe.attachEvent("onload", iframe.onload);
iframe.onload = null;
}
postToIframe(url, data, iframeName);
}

View file

@ -1,29 +0,0 @@
var CallbackRegistry = {}; // реестр
function iframeGet(url, onSuccess, onError) {
var iframeOk = false;
var iframeName = Math.random();
var iframe = createIframe(iframeName, url);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
function iframeOnLoad() { // onload всегда после обработки скриптов в ифрейме
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
iframe.onload = function() { // перенести onload на "после postMessage"
setTimeout(iframeOnLoad, 0);
};
}
window.onmessage = function(event) {
var message = JSON.parse(event.data);
CallbackRegistry[message.name](message.body);
}

View file

@ -1,10 +0,0 @@
<?php
header('Cache-Control: no-cache');
$data = json_encode($_POST['data'] ?: 'Server date: '.date('H:i:s'));
?>
<script>
parent.postMessage(JSON.stringify({
name: window.name,
body: <?=$data?>
}), "http://learn.javascript.ru"); // разрешить читать ответ только с этого хоста
</script>

View file

@ -1,49 +0,0 @@
function createIframe(name, src, debug) {
src = src || 'javascript:false'; // пустой src
var tmpElem = document.createElement('div');
// в старых IE нельзя присвоить name после создания iframe, поэтому создаём через innerHTML
tmpElem.innerHTML = '<iframe name="'+name+'" id="'+name+'" src="'+src+'">';
var iframe = tmpElem.firstChild;
if (!debug) {
iframe.style.display = 'none';
}
document.body.appendChild(iframe);
return iframe;
}
// функция постит объект-хэш content в виде формы с нужным url , target
// напр. postToIframe('/count.php', {a:5,b:6}, 'frame1')
function postToIframe(url, data, target){
var phonyForm = document.getElementById('phonyForm');
if(!phonyForm){
// временную форму создаем, если нет
phonyForm = document.createElement("form");
phonyForm.id = 'phonyForm';
phonyForm.style.display = "none";
phonyForm.method = "POST";
phonyForm.enctype = "multipart/form-data";
document.body.appendChild(phonyForm);
}
phonyForm.action = url;
phonyForm.target = target;
// заполнить форму данными из объекта
var html = [];
for(var key in data){
var value = data[key].replace(/"/g, "&quot;");
html.push("<input type='hidden' name=\""+key+"\" value=\""+value+"\">");
}
phonyForm.innerHTML = html.join('');
phonyForm.submit();
}

View file

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<script src="iframe.js"></script>
<script src="postmessage.js"></script>
<script>
function ok(result) {
alert('ok: '+result);
}
function fail() {
alert('fail');
}
var url = 'http://test.javascript.ru' + location.pathname.replace(/[^\/]*$/, 'anydomain.php');
</script>
<button onclick="iframeGet(url, ok, fail)">iframeGet</button>
</body>
</html>

View file

@ -1,29 +0,0 @@
var CallbackRegistry = {}; // реестр
function iframeGet(url, onSuccess, onError) {
var iframeOk = false;
var iframeName = Math.random();
var iframe = createIframe(iframeName, url);
CallbackRegistry[iframeName] = function(data) {
iframeOk = true;
onSuccess(data);
}
function iframeOnLoad() { // onload всегда после обработки скриптов в ифрейме
iframe.parentNode.removeChild(iframe); // очистка
delete CallbackRegistry[iframeName];
if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так
}
iframe.onload = function() { // перенести onload на "после postMessage"
setTimeout(iframeOnLoad, 0);
};
}
window.onmessage = function(event) {
var message = JSON.parse(event.data);
CallbackRegistry[message.name](message.body);
}

View file

@ -37,7 +37,7 @@
<tr>
<th>Кросс-доменность</th>
<td>да, кроме IE9-<a class="link-ref" href="#x1">x1</a></td>
<td>да, сложности в IE7-<a class="link-ref" href="#i1">i1</a></td>
<td>да<a class="link-ref" href="#i1">i1</a></td>
<td>да</td>
<td>да</td>
<td>да</td>
@ -60,7 +60,7 @@
</tr>
<tr>
<th>Поддержка</th>
<td>Все браузеры, ограничения в IE&lt;10<a class="link-ref" href="#x3">x3</a></td>
<td>Все браузеры, ограничения в IE9-<a class="link-ref" href="#x3">x3</a></td>
<td>Все браузеры</td>
<td>Все браузеры</td>
<td>Кроме IE</td>
@ -83,31 +83,22 @@
<dt>`IFRAME`</dt>
<dd>
<ol>
<li id="i1">Во всех современных браузерах и IE8 кросс-доменность обеспечивает `postMessage`. В более старых браузерах возможны решения через `window.name` и `hash`.</li>
<li id="i1">Во всех современных браузерах и IE8 кросс-доменность обеспечивает `postMessage`. В более старых браузерах возможны решения через `window.name` и хэш.</li>
</ol>
</dd>
<dt>`WebSocket`</dt>
<dd>
<ol>
<li id="w1">Имеется в виду поддержка окончательной редакции протокола [RFC 6455](http://tools.ietf.org/html/rfc6455). Более старые браузеры могут поддерживать черновики протокола. IE9- не поддерживает `WebSocket`.</li>
<li id="w1">Имеется в виду поддержка окончательной редакции протокола [RFC 6455](http://tools.ietf.org/html/rfc6455), описанной в разделе [](/websockets). Более старые браузеры могут поддерживать черновики протокола. IE9- не поддерживает `WebSocket`.</li>
</ol>
</dd>
</dl>
Существует также транспорт, не рассмотренный здесь:
Существует также нестандартный транспорт, не рассмотренный здесь:
<ul>
<li>XMLHttpRequest с флагом `multipart`, только для Firefox.
При указании свойства `xhr.multipart = true` и специального multipart-формата ответа сервера, Firefox инициирует `onload` при получении очередной части ответа.
Ответ может состоять из любого количества частей, досылаемых по инициативе сервера.
При указании свойства `xhr.multipart = true` и специального multipart-формата ответа сервера, Firefox инициирует `onload` при получении очередной части ответа. Ответ может состоять из любого количества частей, досылаемых по инициативе сервера. Мы не рассматривали его, так как Firefox поддерживает другие, более кросс-браузерные и стандартные транспорты.
</li>
</ul>
Мы не рассматривали его, т.к. Firefox поддерживает и другие, стандартные, транспорты.
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

@ -1,86 +0,0 @@
# IFRAME для длинных соединений
Реализация COMET, рассматриваемая здесь, подходит для IE9- и других старых браузеров, не поддерживающих специализированный стандарт WebSocket.
Бесконечный IFRAME -- самый старый способ организации COMET. Когда-то он был основой AJAX-приложений, а сейчас -- используется лишь в случаях, когда браузер не поддерживает современный стандарт WebSocket.
[cut]
Этот способ основан на том, что браузер читает страницу последовательно и обрабатывает все новые теги по мере того, как сервер их присылает.
Классическая реализация -- это когда клиент создает невидимый IFRAME, ведущий на служебный URL. Сервер, получив соединение на этот URL, не закрывает его, а
время от времени присылает блоки сообщений <code>&lt;script&gt;...javascript...&lt;/script&gt;</code>. Появившийся в IFRAME'е javascript тут же выполняется браузером, передавая информацию на основную страницу.
Таким образом, для передачи данных используется "бесконечный" ифрейм, через который сервер присылает все новые данные.
## Схема работы
Принцип работы:
<ol>
<li>Создаётся `<iframe src="COMET_URL">`, по адресу `COMET_URL` расположен сервер.</li>
<li>Сервер выдаёт начало ("шапку") документа и останавливается, оставляя соединение активным.</li>
<li>Когда сервер хочет что-то отправить -- он пишет в соединение <code>&lt;script&gt;parent.onMessage(сообщение)&lt;/script&gt;</code> Браузер тут же выполняет этот скрипт -- так сообщение приходит на клиент.</li>
<li>Ифрейм, в теории, грузится бесконечно. Его завершение означает обрыв канала связи. Его можно поймать по `iframe.onload` и заново открыть соединение (создать новый `iframe`).</li>
</ol>
Также ифрейм можно пересоздавать время от времени, для очистки памяти от старых сообщений.
<img src="comet.png">
Ифрейм работает только на получение данных. Для запросов используется обычный `XMLHttpRequest`.
## Проблемы, специфичные для IFRAME
Такое использование ифреймов является хаком. Поэтому есть ряд проблем:
<ol>
<li>Показывается индикатор загрузки, "курсор-часики".</li>
<li>`IFRAME` влияет на историю посещений, если неправильно создан.</li>
<li>Браузер буферизует начало страницы.</li>
</ol>
Как обойти первые две проблемы в IE6-9 (именно для него в первую очередь и нужен IFRAME), мы видели в главе [](/ajax-iframe-htmlfile).
Что же касается буферизации -- IE не начинает обработку страницы, пока она не загрузится до определенного размера.
Поэтому в таком `IFRAME` первые несколько сообщений задержатся:
```html
<!DOCTYPE HTML>
<html>
<body>
<script>parent.onMessage("привет");</script>
<script>parent.onMessage("от сервера");</script>
...
```
Решение -- забить начало ифрейма чем-нибудь, поставить, например, килобайт пробелов в начале:
```html
<!DOCTYPE HTML>
<html>
<body>
******* 1 килобайт пробелов, а потом уже сообщения ******
<script>parent.onMessage("привет");</script>
<script>parent.onMessage("от сервера");</script>
...
```
## Код
Клиентский код, кросс-браузерно реализующий этот подход с возобновлением соединения при ошибках:
```js
//+ src="iframeComet.js"
```
Его можно улучшить, например сервер может раз в секунду посылать мини-сообщение "я тут", для более надёжного определения статуса подключения.
## Скачать пример
Мини-приложение с сервером на Node.JS, непрерывно получающее текущее время с сервера через `IFRAME`, можно скачать: [comet.zip](/zip/tutorial/ajax/iframe/comet.zip).
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

@ -6,8 +6,6 @@ var file = new static.Server('.');
function accept(req, res) {
console.log(req.url);
if (req.url == '/digits') {
res.writeHead(200, {

View file

@ -1,6 +1,6 @@
# Зачем нужен Origin?
[importance 3]
[importance 5]
Как вы, наверняка, знаете, существует HTTP-заголовок `Referer`, в котором обычно указан адрес страницы, с которой инициирован запрос.

View file

@ -270,7 +270,7 @@ Access-Control-Allow-Credentials: true
Его задача -- спросить сервер, разрешает ли он использовать выбранный метод и заголовки.
На этот запрос сервер должен ответить статусом 200, указав заголовки `Access-Control-Allow-Method: метод` и, если есть заголовки, `Access-Control-Allow-Headers: разрешённые заголовки`.
На этот запрос сервер должен ответить статусом 200, без тела ответа, указав заголовки `Access-Control-Allow-Method: метод` и, при необходимости, `Access-Control-Allow-Headers: разрешённые заголовки`.
Дополнительно он может указать `Access-Control-Max-Age: sec`, где `sec` -- количество секунд, на которые нужно закэшировать разрешение. Тогда при последующих вызовах метода браузер уже не будет делать предзапрос.
@ -319,8 +319,8 @@ Access-Control-Request-Headers: Destination
<ul>
<li>Адрес -- тот же, что и у основного запроса: `http://site.com/~ilya`.</li>
<li>Стандартные заголовки `Accept`, `Accept-Encoding`, `Connection` присутствуют.</li>
<li>Кросс-доменные специальные заголовки:
<li>Стандартные заголовки запроса `Accept`, `Accept-Encoding`, `Connection` присутствуют.</li>
<li>Кросс-доменные специальные заголовки запроса:
<ul>
<li>`Origin` -- домен, с которого сделан запрос.</li>
<li>`Access-Control-Request-Method` -- желаемый метод.</li>
@ -341,6 +341,8 @@ Content-Type: text/plain
*!*Access-Control-Max-Age*/!*: 86400
```
Ответ должен быть без тела, то есть только заголовки.
Браузер видит, что метод `COPY` -- в числе разрешённых и заголовок `Destination` -- тоже, и дальше он шлёт уже основной запрос.
При этом ответ на предзапрос он закэширует на 86400 сек (сутки), так что последующие аналогичные вызовы сразу отправят основной запрос, без `OPTIONS`.
@ -375,3 +377,24 @@ Access-Control-Allow-Origin: http://javascript.ru
Так как `Access-Control-Allow-Origin` содержит правильный домен, то браузер вызовет `xhr.onload` и запрос будет завершён.
## Итого
<ul>
<li>Все современные браузеры умеют делать кросс-доменные XMLHttpRequest.</li>
<li>В IE8,9 для этого используется объект `XDomainRequest`, ограниченный по возможностям.</li>
<li>Кросс-доменный запрос всегда содержит заголовок `Origin` с доменом запроса.</li>
</ul>
Порядок выполнения:
<ol>
<li>Для запросов с "непростым" методом или особыми заголовками браузер делает предзапрос `OPTIONS`, указывая их в `Access-Control-Request-Method` и `Access-Control-Request-Headers`.
Браузер ожидает ответ со статусом `200`, без тела, со списком разрешённых методов и заголовков в `Access-Control-Allow-Method` и `Access-Control-Allow-Headers`. Дополнительно можно указать `Access-Control-Max-Age` для кеширования предзапроса.</li>
<li>Браузер делает запрос и проверяет, есть ли в ответе `Access-Control-Allow-Origin`, равный `*` или `Origin`.
Для запросов с `withCredentials` может быть только `Origin` и дополнительно `Access-Control-Allow-Credentials: true`.</li>
<li>Если проверки пройдены, то вызывается `xhr.onload`, иначе `xhr.onerror`, без деталей ответа.</li>
<li>Дополнительно: названия нестандартных заголовков ответа сервер должен указать в `Access-Control-Expose-Headers`, если хочет, чтобы клиент мог их прочитать.</li>
</ol>
Детали и примеры мы разобрали выше.

View file

@ -0,0 +1,178 @@
# XMLHttpRequest: индикация прогресса
Запрос `XMLHttpRequest` состоит из двух фаз:
<ol>
<li>Стадия закачки (upload). На ней данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов.</li>
<li>Стадия скачивания (download). После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время.</li>
</ol>
Для каждой стадии предусмотрены события, "рассказывающие" о процессе выполнения.
[cut]
## XMLHttpRequestUpload
Для отслеживания прогресса на стадии закачки существует объект [XMLHttpRequestUpload](https://xhr.spec.whatwg.org/#xmlhttprequesteventtarget), доступный как `xhr.upload`.
У него нет методов, он предназначен исключительно для обработки событий при закачке.
Вот их полный список:
<ul>
<li>`loadstart`</li>
<li>`progress`</li>
<li>`abort`</li>
<li>`error`</li>
<li>`load`</li>
<li>`timeout`</li>
<li>`loadend`</li>
</ul>
Пример установки обработчиков на стадию закачки:
```js
xhr.upload.onprogress = function(event) {
alert('Загружено на сервер ' + event.loaded + ' байт из '+ event.total);
}
xhr.upload.onload = function() {
alert('Данные полностью загружены на сервер!');
}
xhr.upload.onerror = function() {
alert('Произошла ошибка при загрузке данных на сервер!');
}
```
После того, как загрузка завершена, будет начато скачивание ответа.
На этой фазе `xhr.upload` уже не нужен, а в дело вступают обработчики событий на самом объекте `xhr`, в частности `xhr.onprogress`:
```js
xhr.onprogress = function(event) {
alert('Получено с сервера ' + event.loaded + ' байт из '+ event.total);
}
```
Все события, возникающие в этих обработчиках, имеют тип [ProgressEvent](https://xhr.spec.whatwg.org/#progressevent), то есть имеют свойства `loaded` -- количество уже пересланных данных в байтах и `total` -- общее количество данных.
## Загрузка файла с индикатором прогресса
Современный `XMLHttpRequest` позволяет отправить на сервер всё, что угодно. Текст, файл, форму.
Мы, для примера, рассмотрим загрузку файла с индикацией прогресса. Это требует от браузера поддержки [File API](http://www.w3.org/TR/FileAPI/), то есть исключает IE9-.
File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag'n'Drop или выбран в поле формы.
Форма для выбора файла с обработчиком `submit`:
```html
<form name="upload">
<input type="file" name="myfile">
<input type="submit" value="Загрузить">
</form>
<script>
document.forms.upload.onsubmit = function() {
var file = this.elements.myfile.files[0];
if (file) {
*!*
upload(file);
*/!*
}
return false;
}
</script>
```
Здесь мы почти не пользуемся File API. Его роль -- получить файл из формы через свойство `files` элемента `<input>` и далее мы сразу передадим его `xhr.send` в функции `upload`:
```js
function upload(file) {
var xhr = new XMLHttpRequest();
// обработчики можно объединить в один,
// если status == 200, то это успех, иначе ошибка
xhr.onload = xhr.onerror = function() {
if(this.status == 200) {
log("success");
} else {
log("error " + this.status);
}
};
// обработчик для закачки
xhr.upload.onprogress = function(event) {
log(event.loaded + ' / '+ event.total);
}
xhr.open("POST", "upload", true);
xhr.send(file);
}
```
Полный пример, основанный на коде выше:
[codetabs src="progress"]
## Событие onprogress в деталях
Событие `onprogress` имеет одинаковый вид при закачке на сервер (`xhr.upload.onprogress`) и при получении ответа (`xhr.onprogress`).
Оно получает объект `event` типа [ProgressEvent](https://xhr.spec.whatwg.org/#progressevent) со свойствами:
<dl>
<dt>`loaded`</dt>
<dd>Сколько байт уже переслано.
Имеется в виду только тело запроса, заголовки не учитываются.</dd>
<dt>`lengthComputable`</dt>
<dd>Если `true`, то известно полное количество байт для пересылки, и оно хранится в свойстве `total`.</dd>
<dt>`total`</dt>
<dd>Общее количество байт для пересылки, если известно.
При HTTP-запросах оно передаётся в заголовке `Content-Length`.
<ul>
<li>При закачке на сервер браузер всегда знает полный размер пересылаемых данных. Поэтому в `xhr.upload.onprogress` значение `lengthComputable` всегда будет `true`, а `total` -- содержать этот размер.</li>
<li>При скачивании данных -- уже задача сервера поставить этот заголовок. Если его нет, то `total = 0`, а для того, чтобы понять, что данных не `0`, а неизвестное количество -- служит `lengthComputable`, которое в данном случае равно `false`.</li>
</ul>
</dd>
</dl>
Ещё особенности, которые необходимо учитывать при использовании `onprogress`:
<ul>
<li>**Событие происходит при каждом полученном/отправленном байте, но не чаще чем раз в 50мс.**
Это обозначено в [спецификации progress notifications](http://www.w3.org/TR/XMLHttpRequest/#make-progress-notifications).
</li>
<li>**При получении данных доступен `xhr.responseText`.**
Можно до окончания запроса заглянуть в него и прочитать текущие полученные данные. Важно, что при пересылке строки в кодировке UTF-8 русские символы кодируются 2 байтами. Возможно, что в конце одного пакета данных окажется первая половинка символа, а в начале следующего -- вторая. Поэтому полагаться на то, что до окончания запроса в `responoseText` находится корректная строка нельзя. Исключение -- заведомо однобайтные символы, например цифры.</li>
<li>**При закачки `xhr.upload.onprogress` не гарантирует обработку загрузки сервером.**
Событие `xhr.upload.onprogress` срабатывает, когда данные отправлены браузером. Но оно не гарантирует, что сервер получил, обработал и записал данные на диск. Он говорит лишь о самом факте отправки.
Поэтому прогресс-индикатор, получаемый при его помощи, носит приблизительный и оптимистичный характер.</li>
</ul>
## Файлы и формы
Выше мы использовали `xhr.send(file)` для посылки файл пересылается в теле запроса.
При этом посылается только содержимое файла.
Если нужно дополнительно передать имя файла или что-то ещё -- это можно удобно сделать через форму, при помощи объекта [FormData](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/FormData/Using_FormData_Objects):
Создадим форму `formData` и прибавим к ней поле с файлом `file` и именем `"myfile"`:
```js
var formData = new FormData();
formData.append("myfile", file);
xhr.send(formData);
```
Данные будут отправлены в кодировке `multipart/form-data`. Серверный фреймворк обработает это как обычную форму.

View file

@ -3,7 +3,7 @@
<body>
<head><meta charset="utf-8"></head>
<form name="upload">
<input type="file" name="file1">
<input type="file" name="myfile">
<input type="submit" value="Загрузить">
</form>
@ -26,31 +26,35 @@ function onProgress(loaded, total) {
log(loaded + ' / '+ total);
}
var form = document.forms.upload;
form.onsubmit = function() {
var file = this.elements.file1.files[0];
if (file) upload(file, onSuccess, onError, onProgress);
document.forms.upload.onsubmit = function() {
var file = this.elements.myfile.files[0];
if (file) {
upload(file);
}
return false;
}
function upload(file, onSuccess, onError, onProgress) {
function upload(file) {
var xhr = new XMLHttpRequest();
// обработчики можно объединить в один,
// если status == 200, то это успех, иначе ошибка
xhr.onload = xhr.onerror = function() {
if(this.status != 200 || this.responseText != 'OK') {
onError(this);
return;
if(this.status == 200) {
log("success");
} else {
log("error " + this.status);
}
onSuccess();
};
// обработчик для закачки
xhr.upload.onprogress = function(event) {
onProgress(event.loaded, event.total);
log(event.loaded + ' / '+ event.total);
}
xhr.open("POST", "upload.php", true);
xhr.open("POST", "upload", true);
xhr.send(file);
}

View file

@ -0,0 +1,37 @@
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var static = require('node-static');
var file = new static.Server('.', { cache: 0 });
function accept(req, res) {
if (req.url == '/upload') {
var length = 0;
req.on('data', function(chunk) {
// ничего не делаем с приходящими данными, просто считываем
length += chunk.length;
if (length > 50*1024*1024) {
res.statusCode = 413;
res.end("File too big");
}
}).on('end', function() {
res.end('ok');
});
} else {
file.serve(req, res);
}
}
// ------ запустить сервер -------
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}

View file

@ -1,160 +0,0 @@
# XMLHttpRequest: индикация прогресса
Запрос `XMLHttpRequest` состоит из двух фаз:
<ol>
<li>На первой фазе данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов.</li>
<li>После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время.</li>
</ol>
Для каждой стадии предусмотрены события, "рассказывающие" о процессе выполнения.
[cut]
## XMLHttpRequestUpload
На стадии загрузки обработчики нужно ставить на объект `xhr.upload`. Например:
```js
xhr.upload.onprogress = function(event) {
alert('Загружено ' + event.loaded + ' байт из '+ event.total);
}
xhr.upload.onload = function() {
alert('Данные полностью загружены на сервер!');
}
```
Объект `xhr.upload` ничего не делает, у него нет методов, он предназначен исключительно для обработки событий при загрузке.
После того, как загрузка завершена, будет начато скачивание ответа.
**Обработчик `xhr.onprogress` даёт прогресс на фазе скачивания, а за ней наступает фаза закачки, для которой работает xhr.upload.onprogress.**
## Загрузка файла с индикатором прогресса
Современный `XMLHttpRequest` позволяет отправить на сервер всё, что угодно. Текст, файл, форму.
Мы, для примера, рассмотрим загрузку файла с индикацией прогресса. Это требует от браузера поддержки [File API](http://www.w3.org/TR/FileAPI/), то есть исключает IE9-.
File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag'n'Drop или выбран в поле формы.
Форма для выбора файла:
```html
<form name="upload">
<input type="file" name="file1">
<input type="submit" value="Загрузить">
</form>
```
Обработчик на её `submit`:
```js
var form = document.forms.upload;
form.onsubmit = function() {
var file = this.elements.file1.files[0];
if (file) *!*upload(file, onSuccess, onError, onProgress)*/!*;
return false;
}
```
Все, больше нам здесь File API не нужно. Мы получили файл из формы и отправляем его:
```js
function upload(file, onSuccess, onError, onProgress) {
var xhr = new XMLHttpRequest();
xhr.onload = xhr.onerror = function() {
if(this.status != 200 || this.responseText != 'OK') {
onError(this);
return;
}
onSuccess();
};
*!*
xhr.upload.onprogress = function(event) {
onProgress(event.loaded, event.total);
}
*/!*
xhr.open("POST", "upload.php", true);
xhr.send(file);
}
```
## Демо
Полный пример, основанный на коде выше, в котором функции `onSuccess`, `onError`, `onProgress` выводят результат на экран:
[iframe border=1 src="progress" link zip]
## Событие onprogress в деталях
Событие `onprogress` имеет одинаковый вид при закачке на сервер (`xhr.upload.onprogress`) и при получении ответа (`xhr.onprogress`).
Оно получает объект `event` типа [ProgressEvent](http://www.w3.org/TR/progress-events/) со свойствами:
<dl>
<dt>`loaded`</dt>
<dd>Сколько байт загружено.
Имеется в виду только тело запроса, заголовки не учитываются.</dd>
<dt>`lengthComputable`</dt>
<dd>Если `true`, то известна полное количество байт и оно хранится в свойстве `total`.
</dd>
<dd>`total`</dd>
<dd>Общее количество байт для загрузки, если известно.
При HTTP-запросах оно передаётся в заголовке `Content-Length`.
<ul>
<li>При закачке на сервер браузер всегда точно знает размер пересылаемого фрагмента, поэтому отправляет этот заголовок и учитывает его в `xhr.upload.onprogress`.</li>
<li>При скачивании данных -- уже задача сервера поставить этот заголовок, если конечно это возможно.</li>
</ul>
</dd>
</dl>
Ещё особенности, которые необходимо учитывать при использовании `onprogress`:
<ul>
<li>**Событие происходит при каждом полученном/отправленном байте, но не чаще чем раз в 50мс.**
Это обозначено в [спецификации progress notifications](http://www.w3.org/TR/XMLHttpRequest/#make-progress-notifications).
</li>
<li>**При получении данных доступен `xhr.responseText`.**
Можно заглянуть в него и прочитать текущие, неоконченные данные. Они будут оборваны на каком-то символе, на каком именно -- предсказать сложно, это зависит от сети.</li>
<li>**При посылке данных не гарантируется обработка загрузки сервером.**
Событие `xhr.upload.onprogress` срабатывает, когда данные отправлены браузером. Но оно не гарантирует, что сервер получил, обработал и записал данные на диск.
Поэтому прогресс-индикатор, получаемый при его помощи, носит приблизительный и оптимистичный характер.</li>
</ul>
## Серверная часть: файлы и формы
При вызове `xhr.send(file)`, файл пересылается в теле запроса, как будто отправлен через форму.
Если нужно дополнительно передать имя файла или что-то ещё -- это можно сделать в заголовках, через `xhr.setRequestHeader`. И, конечно же, серверный фреймворк должен понимать, как это обработать.
А что, если серверный фреймворк умеет нормально работать только с обычными формами, а как-то модифицировать его ну совсем нет желания и времени?
Нам поможет объект [FormData](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/FormData/Using_FormData_Objects):
```js
var formData = new FormData();
formData.append("file", file);
xhr.send(formData);
```
Данные будут отправлены в кодировке `multipart/form-data`. Серверный фреймворк обработает это как обычную форму.
[head]
<script src="/files/tutorial/ajax/script/scriptRequest.js"></script>
[/head]

View file

@ -1,2 +0,0 @@

View file

@ -1,3 +0,0 @@
<?php
/* получить и сохранить данные из php://input */
?>OK

View file

@ -0,0 +1,114 @@
# XMLHttpRequest: возобновляемая закачка
Современный `XMLHttpRequest` даёт возможность загружать файл как угодно: во множество потоков, с догрузкой, с подсчётом контрольной суммы и т.п.
Здесь мы рассмотрим общий подход к организации загрузки, а его уже можно расширять, адаптировать к своему фреймворку и так далее.
Поддержка -- все браузеры кроме IE9-.
[cut]
## Неточный upload.onprogress
Ранее мы рассматривали загрузку с индикатором прогресса. Казалось бы, сделать возобновляемую загрузку на его основе очень просто.
Есть же `xhr.upload.onprogress` -- ставим на него обработчик, по свойству `loaded` события `onprogress` смотрим, сколько байт загрузилось. А при обрыве -- возобновляем загрузку с последнего байта.
К счастью, отослать на сервер не весь файл, а только нужную часть его -- не проблема, [File API](http://www.w3.org/TR/FileAPI/) позволяет прочитать выбранный участок из файла и отправить его.
Примерно так:
```js
var slice = file.slice(10, 100); // прочитать байты с 10го по 99й включительно
xhr.send(slice); // ... и отправить эти байты в запросе.
```
...Но такая модель не жизнеспособна!
Всё дело в том, что `upload.onprogress` срабатывает, когда байты *отправлены*, но были ли они получены сервером -- браузер не знает. Может, их прокси-сервер забуферизовал, может серверный процесс "упал" в процессе обработки, может соединение порвалось и байты так и не дошли до получателя.
**Поэтому `onprogress` годится лишь для красивенького рисования прогресса.**
Для загрузки нам нужно точно знать количество загруженных байт. Это может сообщить только сервер.
## Алгоритм возобновляемой загрузки
Загрузкой файла будет заведовать объект `Uploader`, его примерный общий вид:
```js
function Uploader(file, onSuccess, onFail, onProgress) {
var fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
var errorCount = 0;
var MAX_ERROR_COUNT = 6;
function upload() {
...
}
function pause() {
...
}
this.upload = upload;
this.pause = pause;
}
```
<ul>
<li>Аргументы для `new Uploader`:
<dl>
<dt>`file`</dt>
<dd>Объект File API. Может быть получен из формы, либо как результат Drag'n'Drop.<dd>
<dt>`onSuccess`, `onFail`, `onProgress`</dt>
<dd>Функции-коллбэки, которые будут вызываться в процессе (`onProgress`) и при окончании загрузки.</dd>
</dl>
</li>
<li>Подробнее про важные данные, с которыми мы будем работать в процессе загрузки:
<dl>
<dt>`fileId`</dt>
<dd>Уникальный идентификатор файла, генерируется по имени, размеру и дате модификации. По нему мы всегда сможем возобновить загрузку, в том числе и после закрытия и открытия браузера.</dd>
<dt>`startByte`</dt>
<dd>С какого байта загружать. Изначально -- с нулевого.</dd>
<dt>`errorCount / MAX_ERROR_COUNT`</dt>
<dd>Текущее число ошибок / максимальное число ошибок подряд, после которого загрузка считается проваленной.</dd>
</dl>
</li>
</ul>
Алгоритм загрузки:
<ol>
<li>Генерируем `fileId` из названия, размера, даты модификации файла. Можно добавить и идентификатор посетителя.</li>
<li>Спрашиваем сервер, есть ли уже такой файл, и если да - сколько байт уже загружено?</li>
<li>Отсылаем файл с позиции, которую сказал сервер.</li>
</ol>
При этом загрузку можно прервать в любой момент, просто оборвав все запросы.
Демо ниже, к сожалению, работает лишь частично, так как на этом сайте Node.JS стоит за сервером Nginx, который буферизует все закачки, не передавая их в Node.JS до полного завершения.
Вы можете можете скачать пример и запустить локально для полноценной демонстрации:
[codetabs src="upload-resume" height=160]
Полный код включает также сервер на Node.JS с функциям `onUpload` -- начало и возобновление загрузки, а также `onStatus` -- для получения состояния загрузки.
## Итого
Мы рассмотрели довольно простой алгоритм возобновляемой загрузки.
Его можно усложнить:
<ul>
<li>добавить подсчёт контрольных сумм, проверку целостности пересылаемых файлов,</li>
<li>для индикации прогресса вместо неточного `xhr.upload.onprogress` -- сделать дополнительный запрос к серверу, в который тот будет отдавать текущий прогресс.</li>
<li>разбивать файл на части и грузить в несколько потоков, несколькими параллельными запросами.</li>
</ul>
Как можно видеть, возможности современного XMLHttpRequest в плане загрузки файлов приближаются к полноценному файловому менеджеру -- полный контроль над заголовками, индикатор прогресса и т.п.

View file

@ -3,15 +3,14 @@
<body>
<head>
<meta charset="utf-8">
<script src="browser.js"></script>
<script src="uploader.js"></script>
</head>
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="file1">
<input type="file" name="myfile">
<input type="submit" name="submit" value="Загрузить">
</form>
<button onclick="uploader.pause()">Пауза</button>
<button onclick="uploader.resume()">Возобновить</button>
<div id="log">Индикация прогресса</div>
@ -37,7 +36,7 @@ function onProgress(loaded, total) {
var uploader;
document.forms.upload.onsubmit = function() {
var file = this.elements.file1.files[0];
var file = this.elements.myfile.files[0];
if (!file) return false;
uploader = new Uploader(file, onSuccess, onError, onProgress);

View file

@ -1,29 +1,31 @@
var http = require('http');
var static = require('node-static');
var fileServer = new static.Server('.');
var fs = require('fs');
var path = require('path');
var fs = require('fs');
var uploads = {};
var filesDir = '/tmp';
function onUpload(req, res) {
var uploadId = req.headers['x-upload-id'];
var fileId = req.headers['x-file-id'];
var startByte = req.headers['x-start-byte'];
if (!uploadId) {
res.writeHead(400, "No upload id");
if (!fileId) {
res.writeHead(400, "No file id");
res.end();
}
var filePath = path.join(filesDir, uploadId); // можно положить файл и в другое место
// файлы будм записывать "в никуда"
var filePath = '/dev/null';
// можно положить файл и в реальное место
// var filePath = path.join('/tmp', fileId);
console.log("onUpload fileId: ", fileId);
//console.log("onUpload uploadId: ", uploadId);
// инициализация новой загрузки
if (!uploads[uploadId]) uploads[uploadId] = {};
var upload = uploads[uploadId];
if (!uploads[fileId]) uploads[fileId] = {};
var upload = uploads[fileId];
console.log("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte)
@ -38,6 +40,7 @@ function onUpload(req, res) {
res.end(upload.bytesReceived);
return;
}
// добавляем в существующий файл
fileStream = fs.createWriteStream(filePath, {flags: 'a'});
console.log("File reopened: " + filePath);
}
@ -55,9 +58,9 @@ function onUpload(req, res) {
if (upload.bytesReceived == req.headers['x-file-size']) {
// полностью загрузили
console.log("File finished");
delete uploads[uploadId];
delete uploads[fileId];
fs.unlinkSync(filePath); // удаляю загруженный файл (а мог бы делать с ним что-то еще)
// при необходимости - обработать завершённую загрузку файла
res.end("Success " + upload.bytesReceived);
} else {
@ -77,16 +80,14 @@ function onUpload(req, res) {
}
function onStatus(req, res) {
var uploadId = req.headers['x-upload-id'];
var upload = uploads[uploadId];
console.log("onStatus uploadId:", uploadId, " upload:", upload);
var fileId = req.headers['x-file-id'];
var upload = uploads[fileId];
console.log("onStatus fileId:", fileId, " upload:", upload);
if (!upload) {
res.writeHead(404);
res.end("No such upload");
return;
res.end("0")
} else {
res.end(String(upload.bytesReceived));
}
res.end(String(upload.bytesReceived));
}

View file

@ -2,49 +2,55 @@ function Uploader(file, onSuccess, onFail, onProgress) {
// fileId уникальным образом идентифицирует файл
// можно добавить идентификатор сессии посетителя, но он и так будет в заголовках
// использование: можно добавить в localstorage состояние загрузки по этому ключу
var fileId = file.name + '-' + file.size + '-' + (+file.lastModifiedDate);
var fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
// сделать из fileId число (хеш, алгоритм неважен), мы будем передавать его в заголовке,
// в заголовках разрешены только ASCII-символы
fileId = hashCode(fileId);
// uploadId уникальным образом идентифицирует попытку загрузки
var uploadId = Math.random();
var errorCount = 0;
var MAX_ERROR_COUNT = 6; // максимальное количество ошибок, между которыми нет успешной загрузки
// если количество ошибок подряд превысит MAX_ERROR_COUNT, то стоп
var MAX_ERROR_COUNT = 6;
var startByte = 0;
var xhrUpload;
var xhrStatus;
function resume() {
//console.log("resume: check status");
function upload() {
console.log("upload: check status");
xhrStatus = new XMLHttpRequest();
xhrStatus.onload = xhrStatus.onerror = function() {
if (this.status == 200) {
startByte = +this.responseText || 0;
//console.log("resume: startByte=" + startByte);
upload();
console.log("upload: startByte=" + startByte);
send();
return;
}
// что-то не так
if (errorCount++ < MAX_ERROR_COUNT) {
setTimeout(resume, 1000); // через 1 сек пробуем ещё раз
setTimeout(upload, 1000 * errorCount); // через 1 сек пробуем ещё раз
} else {
onError(this.statusText);
}
};
xhrStatus.open("GET", "status", true);
xhrStatus.setRequestHeader('X-Upload-Id', uploadId);
xhrStatus.send('');
xhrStatus.setRequestHeader('X-File-Id', fileId);
xhrStatus.send();
}
function upload() {
function send() {
xhrUpload = new XMLHttpRequest();
xhrUpload.onload = xhrUpload.onerror = function() {
//console.log("upload end status:" + this.status + " text:"+this.statusText);
console.log("upload end status:" + this.status + " text:" + this.statusText);
if (this.status == 200) {
// успешное завершение загрузки
@ -54,7 +60,7 @@ function Uploader(file, onSuccess, onFail, onProgress) {
// что-то не так
if (errorCount++ < MAX_ERROR_COUNT) {
setTimeout(resume, 1000); // через 1 сек пробуем ещё раз
setTimeout(resume, 1000 * errorCount); // через 1,2,4,8,16 сек пробуем ещё раз
} else {
onError(this.statusText);
}
@ -62,11 +68,7 @@ function Uploader(file, onSuccess, onFail, onProgress) {
xhrUpload.open("POST", "upload", true);
// какой файл догружаем /загружаем
xhrUpload.setRequestHeader('X-Upload-Id', uploadId);
// с какого байта (сервер сверит со своим размером для надёжности)
xhrUpload.setRequestHeader('X-Start-Byte', startByte);
// полный размер загружаемого файла
xhrUpload.setRequestHeader('X-File-Size', file.size);
xhrUpload.setRequestHeader('X-File-Id', fileId);
xhrUpload.upload.onprogress = function(e) {
errorCount = 0;
@ -85,6 +87,18 @@ function Uploader(file, onSuccess, onFail, onProgress) {
this.upload = upload;
this.pause = pause;
this.resume = resume;
}
// вспомогательная функция: получение 32-битного числа из строки
function hashCode(str) {
if (str.length == 0) return 0;
var hash = 0, i, chr, len;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -1,318 +0,0 @@
# XMLHttpRequest: возобновляемая закачка
Современный `XMLHttpRequest` даёт возможность загружать файл как угодно: во множество потоков, с догрузкой, с подсчётом контрольной суммы и т.п.
Здесь мы рассмотрим общий подход к организации загрузки, а его уже можно расширять, адаптировать к своему фреймворку и так далее.
Поддержка -- все браузеры кроме IE9-.
[cut]
## Неточный upload.onprogress
Ранее мы рассматривали загрузку с индикатором прогресса. Казалось бы, сделать возобновляемую загрузку на его основе очень просто.
Есть же `xhr.upload.onprogress` -- ставим на него обработчик, по свойству `loaded` события `onprogress` смотрим, сколько байт загрузилось. А при обрыве -- возобновляем загрузку с последнего байта.
[smart]
К счастью, отослать на сервер нужный отрезок файла -- не проблема, [File API](http://www.w3.org/TR/FileAPI/) позволяет прочитать выбранный участок из файла и отправить его.
Примерно так:
```js
var slice = file.slice(10, 100); // прочитать байты с 10го по 99й включительно
xhr.send(slice); // ... и отправить эти байты в запросе.
```
[/smart]
..Но такая модель не жизнеспособна!
Всё дело в том, что `upload.onprogress` срабатывает, когда байты *отправлены*, но были ли они получены сервером -- браузер не знает. Может, их прокси-сервер забуферизовал, может серверный процесс "упал" в процессе обработки, может соединение порвалось и байты так и не дошли до получателя.
**Поэтому `onprogress` годится лишь для красивенького рисования прогресса.**
Для загрузки нам нужно точно знать количество загруженных байт. Это может сообщить только сервер.
## Общий вид загрузки
Мы разберём несколько способов надёжной загрузки.
У них будет общим объект `Uploader` для загрузки, который имеет примерно следующий вид:
```js
function Uploader(file, onSuccess, onFail, onProgress) {
var uploadId = Math.random();
var startByte = 0;
var fileId = file.name + '-' + file.size + '-' + (+file.lastModifiedDate);
var errorCount = 0;
var MAX_ERROR_COUNT = 6;
function upload() {
...
}
function pause() {
...
}
function resume() {
...
}
this.upload = upload;
this.pause = pause;
this.resume = resume;
}
```
Это -- самый общий интерфейс, разберём его подробнее.
Аргументы:
<dl>
<dt>`file`</dt>
<dd>Объект File API. Может быть получен из формы, либо как результат Drag'n'Drop.<dd>
<dt>`onSuccess`, `onFail`, `onProgress`</dt>
<dd>Функции-коллбэки, которые будут вызываться в процессе (`onProgress`) и при окончании загрузки.</dd>
</dl>
Используемые переменные:
<dl>
<dt>`uploadId`</dt>
<dd>Уникальный идентификатор загрузки. Генерируется случайно. Передаётся на сервер в начале загрузки и при её возобновлении, чтобы сервер знал, что это тот же файл.</dd>
<dt>`startByte`</dt>
<dd>С какого байта загружать. Изначально -- с нулевого.</dd>
<dt>`fileId`</dt>
<dd>Идентификатор файла, основанный на его имени, размере и дате модификации.
По нему текущее состояние загрузки (`uploadId` и `startByte`) можно сохранить в `localStorage`, так что даже при закрытии браузера, а затем открытии и повторной загрузке того же файла -- браузер сможет продолжить загрузку.</dd>
<dt>`errorCount / MAX_ERROR_COUNT`</dt>
<dd>Число ошибок / максимальное число непрерывных ошибок, после которого загрузка считается проваленной.</dd>
</dl>
Использование:
```js
var uploader = new Uploader(form.elements.file.files[0], ...);
uploader.upload(); // загружает с возобновлением
uploader.pause(); // пауза
uploader.resume(); // возобновление загрузки
```
## Способ 1: спрашиваем сервер
Первый способ:
<ol>
<li>Функция `upload` загружает файл обычным образом. При ошибке, если это ещё возможно, вызываем `resume`:
```js
function upload() {
xhrUpload = new XMLHttpRequest();
xhrUpload.onload = xhrUpload.onerror = function() {
if (this.status == 200) {
// успешное завершение загрузки
onSuccess();
return;
}
// что-то не так
if (errorCount++ < MAX_ERROR_COUNT) {
setTimeout(resume, 1000); // через 1 сек пробуем ещё раз
} else {
onError(this.statusText);
}
};
xhrUpload.open("POST", "upload", true);
// какой файл догружаем /загружаем
xhrUpload.setRequestHeader('X-Upload-Id', uploadId);
// с какого байта (сервер сверит со своим размером для надёжности)
xhrUpload.setRequestHeader('X-Start-Byte', startByte);
// полный размер загружаемого файла
xhrUpload.setRequestHeader('X-File-Size', file.size);
xhrUpload.upload.onprogress = function(e) {
errorCount = 0;
onProgress( startByte + e.loaded, startByte + e.total);
}
// отослать, начиная с байта startByte
xhrUpload.send(file.slice(startByte));
}
```
</li>
<li>При ошибке загрузка возобновляется. Но достоверной информацией о том, сколько байт получено, обладает лишь сервер.
Функция `resume` спрашивает его об этом запросом `XMLHttpRequest`, а затем возобновляет загрузку:
```js
function resume() {
xhrStatus = new XMLHttpRequest();
xhrStatus.onload = xhrStatus.onerror = function() {
if (this.status == 200) {
startByte = +this.responseText || 0;
upload();
return;
}
// что-то не так
if (errorCount++ < MAX_ERROR_COUNT) {
setTimeout(resume, 1000); // через 1 сек пробуем ещё раз
} else {
onError(this.statusText);
}
};
xhrStatus.open("GET", "status", true);
xhrStatus.setRequestHeader('X-Upload-Id', uploadId);
xhrStatus.send('');
}
```
</li>
<li>Пауза-возобновление делаются просто:
```js
function pause() {
// обрываем все запросы
xhrStatus && xhrStatus.abort();
xhrUpload && xhrUpload.abort();
}
```
</li>
</ol>
### Демо
[iframe src="upload-resume" link zip border="1" height="160"]
Полный код включает также сервер на Node.JS с функциям `onUpload` -- начало и возобновление загрузки, а также `onStatus` -- для получения состояния загрузки.
## Способ 2: статус с сервера
Предыдущий способ -- прост и хорош. Но для лучшего понимания рассмотрим ещё одну реализацию, которая более точна.
Событие `upload.onprogress`, которое используется для отображения прогресса загрузки -- неточное. Как мы говорили, оно использует данные браузера, а он не может знать, были ли данные успешно получены и, что самое главное, корректно обработаны сервером.
Может быть такое, что индикатор показывает "100%", а на самом деле последний фрагмент ещё не загрузился.
В большинстве случаев эта неточность вполне допустима. Но бывают ситуации, когда мы не имеем права показывать посетителю неточную информацию.
**Описанный далее способ гарантирует точную информацию о процессе загрузке, цена -- дополнительный запрос.**
[warn header="Почему нужен ещё один запрос?"]
Первая идея, которая может прийти в голову -- это получать информацию о состоянии закачки из `xhr.onprogress`, то есть сервер должен при каждом полученном пакете байт писать полученную длину в ответ.
Звучит хорошо, но, к сожалению, в `XMLHttpRequest` браузер *сначала* закачивает данные, а *потом* читает. То есть, сначала сработают события `xhr.upload.onprogress`, `xhr.upload.onload`, а уже затем пойдут события на получение данных: `xhr.onprogress`, `xhr.onload` и другие. Браузер получит ответ сервера целиком уже после окончания загрузки, а нам прогресс нужен -- во время загрузки.
[/warn]
Будем использовать два запроса: в одном -- закачивать, в другом -- читать прогресс с сервера.
<ul>
<li>Функция `runUpload` будет открывать запрос `xhrUpload`, который отправляет данные на сервер. У него даже не будет `onprogress`, всю информацию о загрузке мы будем получать напрямую от сервера.</li>
<li>Функция `trackStatus` будет открывать второй, параллельный, запрос `xhrStatus` и в нём, через `onprogress`, получать данные с сервера.</li>
</ul>
Серверный код будет читать из `xhrUpload`, записывать данные на диск и тут же рапортовать об этом в `xhrStatus`.
Код соответствующей функции `upload`:
```js
//+ src="browser.js"
```
Детали `trackStatus`:
<ul>
<li>В ответ на запрос `xhrStatus` сервер присылает информацию о загрузке с данным идентификатором, дописывая в ответ при поступлении (и, если угодно, записи) очередного пакета -- общий размер загруженных данных.
Ответ сервера выглядит так:
```
1234-56789-123456-200123-423456-...
```
Это означает, что сервер сначала считал `1234` байта, затем получил ещё пакет и стало `56789`..И на текущий момент можно быть уверенными, что `423456` байт загружены.
</li>
<li>Функция `getLastProgress` читает из текущего ответа сервера последнее число. Обратим внимание, что она не может найти последний `-` и взять всё, что после него, т.к. последнее число, возможно, недогружено.
Поэтому из ответа выше она возьмёт `200123`.</li>
<li>Загрузка считается завершённой только после получения `xhrStatus.onload`.</li>
</ul>
Важна тонкость здесь -- в том, что запросы независимы. Любой из них может прийти на сервер первым. Возможно и такое, что закачка завершится до того, как запрос на статус достигнет сервера.
### Демо
Пример вместе с кодом сервера на NodeJS:
[iframe src="upload-2way" link zip border="1" height="160"]
К этому коду можно легко добавить любую логику возобновления загрузки, подсчёта контрольной суммы и т.п. Ведь мы всегда точно знаем, до какого момента сервер получил файл.
## Способ 3: загружаем по частям
Есть ещё один способ. Я привожу его здесь для полноты, так как не раз видел его в статьях в интернет.
Он заключается в том, чтобы разбить файл на части и отсылать их одну за другой.
Примерно так:
```js
function upload() {
// каждая часть 64000 байт,
// можно делать меньше для маленьких файлов и больше для быстрых соединений
var chunk = file.slice(startByte, startByte+64000);
// на текущий момент уже загрузили from байт
onProgress(from, file.size);
var xhr = new XMLHttpRequest();
xhr.onload = function() {
// если всё закачали или сервер ответил ошибку - завершение
// если нет - вызвать загрузку следующей части
upload(file, startByte + 64000);
};
xhr.onerror = function() {
setTimeout(upload, 1000);
};
// ...
xhr.send(chunk);
}
```
Пример заголовков и алгоритма для возобновляемой загрузки: [Спецификация Nginx Upload](http://www.grid.net.ru/nginx/resumable_uploads.ru.html). По ней работает модуль [Nginx Upload Module](http://www.grid.net.ru/nginx/upload.ru.html), но он, к сожалению, нестабилен, на момент написания этих строк замечены падения сервера, да и автор не горит желанием исправлять ошибки и отвечать на вопросы.
Если вы вдруг видите преимущество этого способа перед описанными выше -- напишите об этом в комментариях.
## Итого
Мы рассмотрели 3 способа загрузки файлов через `XMLHttpRequest`:
<ul>
<li>Простой способ, с неточной индикацией прогресса.</li>
<li>Более сложный способ, с увеличенным трафиком, но точной индикацией прогресса.</li>
<li>Загрузка по частям.</li>
</ul>
Все их можно расширить каким угодно образом, включая:
<ul>
<li>Параллельную загрузку одного файла в несколько потоков.</li>
<li>Хранение состояния в `localStorage`.</li>
<li>Подсчёт контрольной суммы при загрузке. Для быстрой проверки рекомендуется алгоритм Adler32 (это быстрый вариант CRC-32 для длинных строк), для безопасной -- MD5, и то и другое реализуется на JavaScript.</li>
</ul>

View file

@ -1,61 +0,0 @@
function Uploader(file, onSuccess, onError, onProgress) {
// идентификатор загрузки, чтобы стыковать два потока
var uploadId = Math.random();
var xhrUpload;
var xhrStatus;
function upload() {
runUpload();
trackStatus();
}
// *!* Закачка: поток НА сервер */!*
function runUpload() {
xhrUpload = new XMLHttpRequest();
xhrUpload.onload = xhrUpload.onerror = function() {
xhrStatus.abort();
if (this.status != 200) {
onError("Upload error " + this.statusText);
} else {
onSuccess(); // не ждем xhrStatus, сервер не обязан хранить статус после завершения upload
}
};
xhrUpload.open("POST", "upload", true);
xhrUpload.setRequestHeader('X-Upload-Id', uploadId);
xhrUpload.send(file);
}
// *!* Получать статус: поток С сервера */!*
function trackStatus() {
xhrStatus = new XMLHttpRequest();
xhrStatus.onprogress = function() {
onProgress( getLastProgress(xhrStatus.responseText), file.size );
}
xhrStatus.onerror = function() {
onError("Upload status error " + this.statusText);
xhrUpload.abort();
}
xhrStatus.open("POST", "status-stream", true);
xhrStatus.setRequestHeader('X-Upload-Id', uploadId);
xhrStatus.send('');
}
// *!* Функция для чтения количества байт из статуса сервера 12-345-... */!*
function getLastProgress(response) {
// читаем число между последним и предпоследним знаком -
var lastDelimiter = response.lastIndexOf('-');
if (lastDelimiter < 0) return 0;
var prevDelimiter = response.lastIndexOf('-', lastDelimiter - 1);
return response.slice(prevDelimiter+1, lastDelimiter);
}
// внешний интерфейс
this.upload = upload;
}

View file

@ -1,4 +1,4 @@
# AJAX и COMET [todo]
# AJAX и COMET
Современный `XMLHttpRequest`, включая поддержку докачки, индикации прогресса, кросс-доменных запросов.

View file

@ -31,7 +31,7 @@
Так как [стандарт CSS Transitions](http://www.w3.org/TR/css3-transitions/) находится в стадии разработки, то `transition` нужно снабжать браузерными префиксами.
### Пример
Этот пример работает во всех современных браузерах, не работает в IE&lt;10.
Этот пример работает во всех современных браузерах, не работает в IE9-.
```html
<style>

View file

@ -217,7 +217,7 @@ function leak() {
Когда запускается асинхронный запрос `xhr`, браузер создаёт специальную внутреннюю ссылку (internal reference) на этот объект. находится в процессе коммуникации. Именно поэтому объект `xhr` будет жив после окончания работы функции.
**Когда запрос завершен, браузер удаляет внутреннюю ссылку, `xhr` становится недостижимым и память очищается... Везде, кроме IE&lt;9.**
**Когда запрос завершен, браузер удаляет внутреннюю ссылку, `xhr` становится недостижимым и память очищается... Везде, кроме IE8-.**
<a href="/files/tutorial/leak/leak_ie8_xhr.html" target="_blank">Открыть в новом окне (в IE8-)</a> (откройте страницу и пусть поработает минут 20 - съест всю память, включая виртуальную).

View file

@ -3,10 +3,10 @@
<body>
<head>
<meta charset="utf-8">
<script src="browser.js"></script>
<script src="uploader.js"></script>
</head>
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="file1">
<input type="file" name="myfile">
<input type="submit" name="submit" value="Загрузить">
</form>
@ -33,7 +33,7 @@ function onProgress(loaded, total) {
var uploader;
document.forms.upload.onsubmit = function() {
var file = this.elements.file1.files[0];
var file = this.elements.myfile.files[0];
if (!file) return false;
uploader = new Uploader(file, onSuccess, onError, onProgress);

View file

@ -1,29 +1,33 @@
var http = require('http');
var static = require('node-static');
var fileServer = new static.Server('.');
var events = require('events');
var EventEmitter = require('events').EventEmitter;
var fs = require('fs');
var path = require('path');
var uploads = {};
var filesDir = '/tmp';
function onUpload(req, res) {
var uploadId = req.headers['x-upload-id'];
if (!uploadId) {
res.writeHead(400, "No upload id");
var fileId = req.headers['x-file-id'];
if (!fileId) {
res.writeHead(400, "No file id");
res.end();
}
// инициализация новой загрузки
var upload = uploads[uploadId] = new events.EventEmitter();
var upload = uploads[fileId];
if (!upload) {
res.writeHead(400, "No such upload");
res.end();
return;
}
upload.bytesWritten = 0;
var filePath = path.join(filesDir, uploadId);
var filePath = '/dev/null'; //path.join("/tmp", fileId);
// будем сохранять в файл с именем uploadId
// будем сохранять в файл с именем fileId
var fileStream = fs.createWriteStream(filePath);
// отправить тело запроса в файл
@ -50,42 +54,31 @@ function onUpload(req, res) {
// в конце запроса - очистка
req.on('end', function() {
res.end();
delete uploads[uploadId];
// в этой демке мы не оставляем файл на диске,
// в реальности здесь будет обработка успешной загрузки
fs.unlinkSync(filePath);
delete uploads[fileId];
// здесь обработка успешной загрузки
});
}
function onStatusStream(req, res) {
var uploadId = req.headers['x-upload-id'];
var upload = uploads[uploadId];
var fileId = req.headers['x-file-id'];
var upload = uploads[fileId];
// нет такой загрузки?
// подождём немного, может почему-то запрос на статус дошёл до сервера раньше,
// и она ещё не началась
if (!upload) {
var interval = setTimeout(function() {
onStatusStream(req, res)
}, 500);
return;
upload = uploads[fileId] = new EventEmitter();
upload.bytesWritten = 0;
}
// ..при окончании запроса на статус - прекращаем ждать
req.on('end', function() {
clearInterval(interval);
});
// есть загрузка
console.log("upload bytesWritten", upload.bytesWritten);
// сразу же пишем текущий прогресс
res.write(upload.bytesWritten+'-');
res.write(upload.bytesWritten + '-');
// и по мере загрузки дописываем
upload.on('progress', function() {
res.write(upload.bytesWritten+'-');
res.write(upload.bytesWritten + '-');
});
upload.on('end', function() {

View file

@ -1,38 +1,44 @@
function Uploader(file, onSuccess, onError, onProgress) {
// идентификатор загрузки, чтобы стыковать два потока
var uploadId = Math.random();
var fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
fileId = hashCode(fileId);
var xhrUpload;
var xhrStatus;
function upload() {
runUpload();
trackStatus();
}
// *!* Закачка: поток НА сервер */!*
function runUpload() {
function runUpload(startByte) {
console.log("run upload from ", startByte);
xhrUpload = new XMLHttpRequest();
xhrUpload.onload = xhrUpload.onerror = function() {
xhrStatus.abort();
xhrStatus.abort(); // остановить отслеживание прогресса по завершении загрузки
if (this.status != 200) {
onError("Upload error " + this.statusText);
} else {
onSuccess(); // не ждем xhrStatus, сервер не обязан хранить статус после завершения upload
onSuccess();
}
};
xhrUpload.open("POST", "upload", true);
xhrUpload.setRequestHeader('X-Upload-Id', uploadId);
xhrUpload.send(file);
xhrUpload.setRequestHeader('X-File-Id', fileId);
xhrUpload.send(file.slice(startByte));
}
// *!* Получать статус: поток С сервера */!*
function trackStatus() {
function upload() {
xhrStatus = new XMLHttpRequest();
xhrStatus.onreadystatechange = function(e) {
console.log(this);
}
xhrStatus.onprogress = function() {
onProgress( getLastProgress(xhrStatus.responseText), file.size );
var lastByte = getLastProgress(xhrStatus.responseText);
console.log("lastByte", lastByte);
if (!xhrUpload) {
runUpload(lastByte+1);
}
onProgress(lastByte, file.size);
}
xhrStatus.onerror = function() {
@ -41,8 +47,8 @@ function Uploader(file, onSuccess, onError, onProgress) {
}
xhrStatus.open("POST", "status-stream", true);
xhrStatus.setRequestHeader('X-Upload-Id', uploadId);
xhrStatus.send('');
xhrStatus.setRequestHeader('X-File-Id', fileId);
xhrStatus.send();
}
// *!* Функция для чтения количества байт из статуса сервера 12-345-... */!*
@ -59,3 +65,19 @@ function Uploader(file, onSuccess, onError, onProgress) {
this.upload = upload;
}
// вспомогательная функция: получение 32-битного числа из строки
function hashCode(str) {
if (str.length == 0) return 0;
var hash = 0, i, chr, len;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};