Запрос XMLHttpRequest состоит из двух фаз:
- Стадия отправки (upload). На ней данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов. Для отслеживания прогресса на стадии отправки существует объект типа XMLHttpRequestUpload, доступный как
xhr.uploadи события на нём. - Стадия скачивания (download). После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время. На этой стадии используется обработчик
xhr.onprogress.
Далее – обо всём по порядку.
Стадия отправки
На стадии отправки для получения информации используем объект xhr.upload. У этого объекта нет методов, он только генерирует события в процессе отправки. А они-то как раз и нужны.
Вот полный список событий:
loadstartprogressaborterrorloadtimeoutloadend
Пример установки обработчиков на стадию отправки:
xhr.upload.onprogress = function(event) {
alert( 'Загружено на сервер ' + event.loaded + ' байт из ' + event.total );
}
xhr.upload.onload = function() {
alert( 'Данные полностью загружены на сервер!' );
}
xhr.upload.onerror = function() {
alert( 'Произошла ошибка при загрузке данных на сервер!' );
}
Стадия скачивания
После того, как загрузка завершена, и сервер соизволит ответить на запрос, XMLHttpRequest начнёт скачивание ответа сервера.
На этой фазе xhr.upload уже не нужен, а в дело вступают обработчики событий на самом объекте xhr. В частности, событие xhr.onprogress содержит информацию о количестве принятых байт ответа.
Пример обработчика:
xhr.onprogress = function(event) {
alert( 'Получено с сервера ' + event.loaded + ' байт из ' + event.total );
}
Все события, возникающие в этих обработчиках, имеют тип ProgressEvent, то есть имеют свойства loaded – количество уже пересланных данных в байтах и total – общее количество данных.
Демо: загрузка файла с индикатором прогресса
Современный XMLHttpRequest позволяет отправить на сервер всё, что угодно. Текст, файл, форму.
Мы, для примера, рассмотрим загрузку файла с индикацией прогресса. Это требует от браузера поддержки File API, то есть исключает IE9-.
File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag’n’Drop или выбран в поле формы, и отправить его при помощи XMLHttpRequest.
Форма для выбора файла с обработчиком submit:
<form name="upload">
<input type="file" name="myfile">
<input type="submit" value="Загрузить">
</form>
<script>
document.forms.upload.onsubmit = function() {
var input = this.elements.myfile;
var file = input.files[0];
if (file) {
upload(file);
}
return false;
}
</script>
Мы получаем файл из формы через свойство files элемента <input> и передаём его в функцию upload:
function upload(file) {
var xhr = new XMLHttpRequest();
// обработчик для отправки
xhr.upload.onprogress = function(event) {
log(event.loaded + ' / ' + event.total);
}
// обработчики успеха и ошибки
// если status == 200, то это успех, иначе ошибка
xhr.onload = xhr.onerror = function() {
if (this.status == 200) {
log("success");
} else {
log("error " + this.status);
}
};
xhr.open("POST", "upload", true);
xhr.send(file);
}
Этот код отправит файл на сервер и будет сообщать о прогрессе при его отправке (xhr.upload.onprogress), а также об окончании запроса (xhr.onload, xhr.onerror).
Полный пример индикации прогресса при загрузке, основанный на коде выше:
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;
}<!DOCTYPE HTML>
<html>
<body>
<head>
<meta charset="utf-8">
</head>
<form name="upload">
<input type="file" name="myfile">
<input type="submit" value="Загрузить">
</form>
<div id="log">Прогресс загрузки</div>
<script>
function log(html) {
document.getElementById('log').innerHTML = html;
}
document.forms.upload.onsubmit = function() {
var file = this.elements.myfile.files[0];
if (file) {
upload(file);
}
return false;
}
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);
}
</script>
</body>
</html>Событие onprogress в деталях
При обработке события onprogress есть ряд важных тонкостей.
Можно, конечно, их игнорировать, но лучше бы знать.
Заметим, что событие, возникающее при onprogress, имеет одинаковый вид на стадии отправки (в обработчике xhr.upload.onprogress) и при получении ответа (в обработчике xhr.onprogress).
Оно представляет собой объект типа ProgressEvent со свойствами:
loaded-
Сколько байт уже переслано.
Имеется в виду только тело запроса, заголовки не учитываются.
lengthComputable-
Если
true, то известно полное количество байт для пересылки, и оно хранится в свойствеtotal. total-
Общее количество байт для пересылки, если известно.
А может ли оно быть неизвестно?
- При отправке на сервер браузер всегда знает полный размер пересылаемых данных, так что
totalвсегда содержит конкретное количество байт, а значениеlengthComputableвсегда будетtrue. - При скачивании данных – обычно сервер в начале сообщает их общее количество в HTTP-заголовке
Content-Length. Но он может и не делать этого, например если сам не знает, сколько данных будет или если генерирует их динамически. Тогдаtotalбудет равно0. А чтобы отличить нулевой размер данных от неизвестного – как раз служитlengthComputable, которое в данном случае равноfalse.
Ещё особенности, которые необходимо учитывать при использовании onprogress:
-
Событие происходит при каждом полученном/отправленном байте, но не чаще чем раз в 50 мс.
Это обозначено в спецификации progress notifications.
-
В процессе получения данных, ещё до их полной передачи, доступен
xhr.responseText, но он не обязательно содержит корректную строку.Можно до окончания запроса заглянуть в него и прочитать текущие полученные данные. Важно, что при пересылке строки в кодировке UTF-8 кириллические символы, как, впрочем, и многие другие, кодируются 2 байтами. Возможно, что в конце одного пакета данных окажется первая половинка символа, а в начале следующего – вторая. Поэтому полагаться на то, что до окончания запроса в
responseTextнаходится корректная строка нельзя. Она может быть обрезана посередине символа.Исключение – заведомо однобайтные символы, например цифры или латиница.
-
Сработавшее событие
xhr.upload.onprogressне гарантирует, что данные дошли.Событие
xhr.upload.onprogressсрабатывает, когда данные отправлены браузером. Но оно не гарантирует, что сервер получил, обработал и записал данные на диск. Он говорит лишь о самом факте отправки.Поэтому прогресс-индикатор, получаемый при его помощи, носит приблизительный и оптимистичный характер.
Файлы и формы
Выше мы использовали xhr.send(file) для передачи файла непосредственно в теле запроса.
При этом посылается только содержимое файла.
Если нужно дополнительно передать имя файла или что-то ещё – это можно удобно сделать через форму, при помощи объекта FormData:
Создадим форму formData и прибавим к ней поле с файлом file и именем "myfile":
var formData = new FormData();
formData.append("myfile", file);
xhr.send(formData);
Данные будут отправлены в кодировке multipart/form-data. Серверный фреймворк увидит это как обычную форму с файлом, практически все серверные технологии имеют их встроенную поддержку. Индикация прогресса реализуется точно так же.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)