JavaScript
June 2011 8

Новые возможности XMLHttpRequest2

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

Наш старый друг XMLHttpRequest сильно изменился, но не многие знают о его изменениях. XMLHttpRequest Level 2 включает в себя новые возможности, которые положат конец нашим безумным хакам и пляскам с бубном вокруг XMLHttpRequest: кросс-доменные запросы, процесс загрузки файлов, загрузка и отправка двоичных данных. Эти возможности позволяют AJAX уверенно работать без каких-либо хаков с новейшими технологиями HTML5: File System API, Web Audio API, и WebGL.

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

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


Извлечение двоичных данных из файла в XHR очень болезненно. Технически это даже невозможно. Но есть один хорошо документированный трюк, который позволяет переписать mime-тип пользовательской кодировкой.

Вот так раньше можно было получить содержимое картинки:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);

// Хак для того, чтобы байты были переданы неизменными
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;
    }
  }
};

xhr.send();

Хотя это работает, но вы получаете в responseText не binary blob, а бинарную строку, которая представляет бинарный файл картинки. Мы обманываем XMLHttpRequest и заставляем его пропускать данные необработанными. Хотя это маленький хак, но я хочу его назвать черной магией.

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


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

xhr.responseType
Перед отправкой запроса можно изменить свойство xhr.responseType и указать формат выдачи: «text», «arraybuffer», «blob» или «document» (по умолчанию «text»).

xhr.response
После выполнения удачного запроса свойство response будет содержать запрошенные данные в формате DOMString, ArrayBuffer, Blob или Document в соответствии с responseType.

С этой новой замечательной фичей мы можем переделать предыдущий пример. На этот раз мы запросом картинку как ArrayBuffer вместо строки. Выгруженный файл мы переделаем в формат Blob с помощью BlobBuilder API:
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); // Внимание: НЕ xhr.responseText

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

xhr.send();

Вот так намного лучше!

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

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]; // 4-й байт
  /*...*/
};

xhr.send();

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

Если вы желаете работать напрямую с Blob и/или вам не нужно манипулировать байтами файла используйте xhr.responseType='blob' (Сейчас есть только в Chrome crbug.com/52486):
window.URL = window.URL || window.webkitURL;

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 может быть использован в нескольких местах: сохранение данных в indexedDB, запись в HTML5 File System, создание Blob URL(MDC) как в примере выше.

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


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

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

До XMLHttpRequest 2:
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');

После XMLHttpRequest 2:
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, но вы можете не указывать responseType и получите аналогичный результат (по умолчанию всегда text).

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

Думаю многие из вас использовали jQuery или другие библиотеки для отправки данных формы по AJAX. Вместо этого мы можем использовать FormData ещё один тип данных, который понимает XHR2. FormData удобен для создания HTML форм на лету в 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); // <<<

По существу, мы динамически создаем форму и добавляем в неё поля 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 из HTMLFormElement 

  formData.append('secret_token', '1234567890'); // Добавляем дополнительные данные перед отправкой

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

  xhr.send(formData);

  return false; // Предотвращаем отправку 
}

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)

Используя XHR2 мы также можем отправить File или Blob. Имейте ввиду, что файлы это и есть Blob.
В этом примере мы создадим с нуля новое текстовое поле, используя BlobBuilder API и загрузим этот Blob на сервер. Этот код также создает обработчик, который показывает нам процесс загрузки файла (Невероятно полезная фича HTML5):
<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) { /*...*/ };

  // Слушаем процесс загрузки файла
  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function(e) { // <<<
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      progressBar.textContent = progressBar.value; // Если браузер не поддерживает элемент progress
    }
  };

  xhr.send(blobOrFile); // <<<
}

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

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

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

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

Мы можем отправить ArrayBuffers
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 запрос, то запрос не будет выполнен и браузер выбросит исключение «origin mismatch». С 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:
image
Включить кросс-доменные запросы очень просто. Если ваши данные доступны для всех, то, пожалуйста, включите 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();

Все предельно прозрачно и никаких плясок с бубном вокруг postMessage, window.name, document.domain, серверных проксей и прочих извращенийметодов.

Примеры


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

Предположим, что у нас есть галерея изображений и мы хотим сохранить несколько картинок к себе, используя HTML5 File System.
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) { /*...*/ };
        
        // Создаем Blob с данными картинки
        var bb = new BlobBuilder(); // <<<
        bb.append(this.response);   // <<<

        // Пишем в файл
        writer.write(bb.getBlob('image/png')); // <<<

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

xhr.send();

Внимание: посмотрите какие браузеры поддерживают FileSystem API

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

Используя File API мы можем упростить процесс отправки большого файла. Мы разбиваем большой файл на несколько маленьких файлов потом каждый оправляем с помощью XHR. На сервере собираем файл в один большой. Это похоже на то как GMail отправляет большие вложения. Такая техника может применяться для обхода ограничений Google App Engine — 32MB на один 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
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {

    // Внимание: blob.slice поменял семантику. Подробнее 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);

})();

Скрипт сборки файла на сервере не прикладываю — там все очевидно.

Ссылки


1. Спецификация XMLHttpRequest Level 2
2. Спецификация Cross Origin Resource Sharing (CORS)
3. Спецификация File API
4. Спецификация FileSystem API
+125
66k 411
Comments 32