XMLHttpRequest2: новые приемы

HTML5 Rocks

Введение

Одним из важнейших элементов в среде HTML5 является XMLHttpRequest. Строго говоря, этот объект не входит в HTML5. Однако он стал результатом постоянных изменений, вносимых разработчиками браузеров в базовую платформу. XHR2 играет большую роль, так как является неотъемлемой частью современных сложных веб-приложений.

Мало кто знает, что в последнюю версию XHR было добавлено много функций. В XMLHttpRequest Level 2 представлена масса новых возможностей, которые избавят нас от ненужных операций и таких понятий, как кросс-доменные запросы, события хода отправки файлов, а также поддержка загрузки и отправки двоичных данных. Благодаря этому технология AJAX работает в сочетании с новейшими API HTML5: API файловой системы, API веб-аудио и WebGL.

В этом руководстве описываются некоторые из новых возможностей XMLHttpRequest и в особенности те из них, которые необходимы для работы с файлами.

Извлечение данных

Загрузка файла в виде двоичного объекта с помощью XHR всегда была проблемой. С технической точки зрения это было даже невозможно. Один из известных способов заключается в переопределении mime-типа пользовательской кодировкой, как показано ниже.

Ранее содержимое картинки можно было извлечь таким способом:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);

// Hack to pass bytes through unprocessed.
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
    var binStr = this.responseText;
    for (var i = 0, len = binStr.length; i < len; ++i) {
      var c = binStr.charCodeAt(i);
      //String.fromCharCode(c & 0xff);
      var byte = c & 0xff;  // byte at offset i
    }
  }
};

xhr.send();

Этот способ работает, однако элемент responseText вовсе не является большим двоичным объектом (элементом blob). Это двоичная строка, представляющая файл картинки. Мы заставляем сервер вернуть данные в необработанном виде. Хотя этот прием работает, я не рекомендую использовать его. При попытке принудительно перевести данные в нужный формат с помощью манипуляций с кодировкой и строками всегда возникают проблемы.

Указание формата ответа

В предыдущем примере картинка загружалась в виде двоичного файла путем переопределения mime-типа сервера и обработки текста как двоичной строки. Вместо этого воспользуемся новыми возможностями технологии XMLHttpRequest: свойствами responseType и response, позволяющими указать браузеру желаемый формат ответа.

xhr.responseType
Прежде чем отправить запрос, необходимо задать для свойства xhr.responseType значение text, arraybuffer, blob или document. Обратите внимание: если установить значение xhr.responseType = '' или опустить его, по умолчанию выбирается формат text.
xhr.response
После успешного выполнения запроса свойство response будет содержать запрошенные данные в формате DOMString, ArrayBuffer, Blob или Document в соответствии со значением responseType.

Переработаем предыдущий пример с использованием этой новой возможности. Теперь мы извлекаем данные картинки в формате ArrayBuffer вместо строки. Передаем буфер в API BlobBuilder и получаем объект Blob.

BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  if (this.status == 200) {
    var bb = new BlobBuilder();
    bb.append(this.response); // Note: not xhr.responseText

    var blob = bb.getBlob('image/png');
    ...
  }
};

xhr.send();

Так намного лучше.

Ответы в формате ArrayBuffer

ArrayBuffer – это стандартный контейнер фиксированной длины для двоичных данных. Это очень удобный универсальный буфер для необработанной информации, но его главное достоинство – возможность создавать "представления" исходных данных с помощью типизированных массивов JavaScript. Фактически на базе одного источника ArrayBuffer можно сформировать несколько представлений. Например, можно создать 8-битный целочисленный массив, который использует тот же объект ArrayBuffer, что и 32-битный массив на базе тех же данных. Исходная информация остается неизменной: она просто представляется в разном виде.

В примере ниже мы извлекаем ту же картинку в формате ArrayBuffer, но на этот раз создаем из данных в буфере 8-битный целочисленный массив.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response); // this.response == uInt8Array.buffer
  // var byte3 = uInt8Array[4]; // byte at offset 4
  ...
};

xhr.send();

Ответы в формате Blob

Для непосредственной работы с объектами Blob без операций с отдельными байтами файла можно использовать значение xhr.responseType='blob'.

window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    img.onload = function(e) {
      window.URL.revokeObjectURL(img.src); // Clean up after yourself.
    };
    img.src = window.URL.createObjectURL(blob);
    document.body.appendChild(img);
    ...
  }
};

xhr.send();

Объект Blob можно использовать по разному: например, сохранить его в индексированной базе данных, записать в файловую систему HTML5 или создать URL элемента Blob, как показано в этом примере.

Отправка данных

Возможность загружать данные в различных форматах очень важна, но она совершенно бесполезна, если эти данные нельзя отправить обратно (на сервер). До недавнего времени в XMLHttpRequest можно было отправлять только данные DOMString или Document (XML). Ситуация изменилась. Переработанный метод send() позволяет отправлять данные любых типов: DOMString, Document, FormData, Blob, File и ArrayBuffer. Примеры в этой части раздела иллюстрируют отправку данных каждого из этих типов.

Отправка строковых данных: xhr.send(DOMString)

function sendText(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.responseText);
    }
  };

  xhr.send(txt);
}

sendText('test string');
function sendTextNew(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.responseType = 'text';
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.response);
    }
  };
  xhr.send(txt);
}

sendText2('test string');

Как видите, ничего нового. Хотя правая часть несколько отличается. В ней есть строка responseType='text'. Впрочем, ее отсутствие не меняет результат.

Отправка данных форм: xhr.send(FormData)

Многие из нас привыкли пользоваться плагинами jQuery и другими библиотеками для отправки форм AJAX. Вместо них можно использовать FormData – еще один новый тип данных в рамках технологии XHR2. Тип FormData очень удобен для динамического создания HTML-элементов <form> с помощью JavaScript. Затем эти формы можно отправить с помощью AJAX.

function sendForm() {
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);
}

По сути, мы просто динамически создаем элемент <form> и добавляем в нее поля <input> с помощью метода append.

При этом форму можно не создавать с нуля. Объекты FormData можно инициализировать с помощью существующих на странице элементов HTMLFormElement. Например:

<form id="myform" name="myform" action="/server">
  <input type="text" name="username" value="johndoe">
  <input type="number" name="id" value="123456">
  <input type="submit" onclick="return sendForm(this.form);">
</form>
function sendForm(form) {
  var formData = new FormData(form);

  formData.append('secret_token', '1234567890'); // Append extra data before send.

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);

  return false; // Prevent page from submitting.
}

HTML-форма может содержать файлы (например, <input type="file">). Объект FormData тоже поддерживает эту возможность. Достаточно просто прикрепить файлы, и браузер выполнит запрос multipart/form-data при вызове метода send().

function uploadFiles(url, files) {
  var formData = new FormData();

  for (var i = 0, file; file = files[i]; ++i) {
    formData.append(file.name, file);
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);  // multipart/form-data
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  uploadFiles('/server', this.files);
}, false);

Отправка файла или объекта Blob: xhr.send(Blob)

С помощью XHR также можно отправить файл или объект Blob. Следует помнить, что файлы являются объектами Blob, поэтому в наших примерах между ними нет разницы.

В этом примере мы создаем новый текстовый файл с помощью API BlobBuilder и отправляем этот объект Blob на сервер. Этот код также запускает обработчик, который показывает нам ход отправки файла.

<progress min="0" max="100" value="0">0% complete</progress>
function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  // Listen to the upload progress.
  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
    }
  };

  xhr.send(blobOrFile);
}

// Take care of vendor prefixes.
BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

var bb = new BlobBuilder();
bb.append('hello world');

upload(bb.getBlob('text/plain'));

Отправка произвольного набора байтов: xhr.send(ArrayBuffer)

В качестве полезных данных XHR также можно отправлять объекты ArrayBuffer.

function sendArrayBuffer() {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  var uInt8Array = new Uint8Array([1, 2, 3]);

  xhr.send(uInt8Array.buffer);
}

Обмен ресурсами с запросом происхождения (Cross Origin Resource Sharing, или CORS)

С помощью технологии CORS веб-приложения могут выполнять кросс-доменные AJAX-запросы к другим доменам. Сделать это очень просто: достаточно, чтобы сервер отправил необходимый заголовок ответа.

Включение CORS-запросов

Предположим, приложение находится в домене example.com и нужно получить данные из домена www.example2.com. Как правило, при попытке отправить такой AJAX-запрос он не выполняется, а браузер выдает ошибку несоответствия происхождения. Благодаря технологии CORS сайт www.example2.com может разрешить приложению с сайта example.com выполнять запросы путем добавления одного заголовка.

Access-Control-Allow-Origin: http://example.com

Заголовок Access-Control-Allow-Origin можно добавить как для одного сайта, так и для всего домена. Чтобы разрешить отправку запросов из всех доменов, добавьте строку такого вида:

Access-Control-Allow-Origin: *

Фактически на всех страницах этого сайта (html5rocks.com) также используется технология CORS. Запустите инструменты разработчика, и в ответе вы увидите заголовок Access-Control-Allow-Origin:

Заголовок Access-Control-Allow-Origin на сайте html5rocks.com
Заголовок Access-Control-Allow-Origin на сайте html5rocks.com

Разрешить кросс-доменные запросы несложно, поэтому настоятельно рекомендуется включать CORS для общедоступных данных.

Создание кросс-доменного запроса

Если сервер-адресат поддерживает CORS, кросс-доменный запрос ничем не отличается от обычного запроса XMLHttpRequest. Например, вот так можно выполнить запрос с приложения на сервере example.com к серверу www.example2.com:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

Практические примеры

Загрузка и сохранение файлов в файловой системе HTML5

Предположим, у вас есть галерея изображений и вы хотите сохранить несколько картинок у себя с помощью файловой системы HTML5. Вы можете запросить эти картинки как объекты ArrayBuffer, создать на основе этих данных объект Blob и записать его с помощью FileWriter.

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

function onError(e) {
  console.log('Error', e);
}

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {

  window.requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
    fs.root.getFile('image.png', {create: true}, function(fileEntry) {
      fileEntry.createWriter(function(writer) {

        writer.onwrite = function(e) { ... };
        writer.onerror = function(e) { ... };

        var bb = new BlobBuilder();
        bb.append(xhr.response);

        writer.write(bb.getBlob('image/png'));

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

Обратите внимание: для использования этого кода нужно ознакомиться с условиями поддержки браузеров и ограничениями на хранение в руководстве Знакомство с API файловой системы.

Отправка файла по частям

API файлов существенно облегчает отправку больших файлов. Методика такова: крупный файл разбивается на несколько мелких, которые затем отправляются с помощью XHR и собираются обратно на сервере. Примерно так же Gmail быстро отправляет большие прикрепленные файлы. Эта технология также позволяет обойти ограничение Google App Engine: 32 МБ на один HTTP-запрос.

window.BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder ||
                     window.BlobBuilder;

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };
  xhr.send(blobOrFile);
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  var blob = this.files[0];

  const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {

    // Note: blob.slice has changed semantics and been prefixed. See http://goo.gl/U9mE5.
    if ('mozSlice' in blob) {
      var chunk = blob.mozSlice(start, end);
    } else {
      var chunk = blob.webkitSlice(start, end);
    }

    upload(chunk);

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

Ниже приведен код для сборки файла на сервере.

Проверьте, как он работает.

#bytes/chunk:

Полезные ссылки

Comments

0