en.javascript.info/4-ajax/6-xhr-onprogress/article.md
2015-02-27 13:21:58 +03:00

9.3 KiB
Raw Blame History

XMLHttpRequest: индикация прогресса

Запрос XMLHttpRequest состоит из двух фаз:

  1. Стадия закачки (upload). На ней данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов.
  2. Стадия скачивания (download). После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время.

Для каждой стадии предусмотрены события, "рассказывающие" о процессе выполнения.

[cut]

XMLHttpRequestUpload

Для отслеживания прогресса на стадии закачки существует объект XMLHttpRequestUpload, доступный как xhr.upload.

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

Вот их полный список:

  • `loadstart`
  • `progress`
  • `abort`
  • `error`
  • `load`
  • `timeout`
  • `loadend`

Пример установки обработчиков на стадию закачки:

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:

xhr.onprogress = function(event) {
  alert('Получено с сервера ' + event.loaded + ' байт из '+ event.total);
}

Все события, возникающие в этих обработчиках, имеют тип ProgressEvent, то есть имеют свойства loaded -- количество уже пересланных данных в байтах и total -- общее количество данных.

Загрузка файла с индикатором прогресса

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

Мы, для примера, рассмотрим загрузку файла с индикацией прогресса. Это требует от браузера поддержки File API, то есть исключает IE9-.

File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag'n'Drop или выбран в поле формы.

Форма для выбора файла с обработчиком submit:

<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:

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 со свойствами:

`loaded`
Сколько байт уже переслано.

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

`lengthComputable`
Если `true`, то известно полное количество байт для пересылки, и оно хранится в свойстве `total`.
`total`
Общее количество байт для пересылки, если известно.

При HTTP-запросах оно передаётся в заголовке Content-Length.

  • При закачке на сервер браузер всегда знает полный размер пересылаемых данных. Поэтому в `xhr.upload.onprogress` значение `lengthComputable` всегда будет `true`, а `total` -- содержать этот размер.
  • При скачивании данных -- уже задача сервера поставить этот заголовок. Если его нет, то `total = 0`, а для того, чтобы понять, что данных не `0`, а неизвестное количество -- служит `lengthComputable`, которое в данном случае равно `false`.

Ещё особенности, которые необходимо учитывать при использовании onprogress:

  • **Событие происходит при каждом полученном/отправленном байте, но не чаще чем раз в 50мс.**

    Это обозначено в спецификации progress notifications.

  • **При получении данных доступен `xhr.responseText`.**

    Можно до окончания запроса заглянуть в него и прочитать текущие полученные данные. Важно, что при пересылке строки в кодировке UTF-8 русские символы кодируются 2 байтами. Возможно, что в конце одного пакета данных окажется первая половинка символа, а в начале следующего -- вторая. Поэтому полагаться на то, что до окончания запроса в responoseText находится корректная строка нельзя. Исключение -- заведомо однобайтные символы, например цифры.

  • **При закачки `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. Серверный фреймворк обработает это как обычную форму.