Новые трюки в XMLHttpRequest2

Введение

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

Оказывается, наш старый друг претерпел огромные изменения, но многие люди не знают о его новых функциях. 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 не двоичный объект. Это двоичная строка, представляющая файл изображения. Мы обманом заставляем сервер передавать данные обратно в необработанном виде. Несмотря на то, что эта маленькая жемчужина работает, я назову ее черной магией и посоветую воздержаться от нее. Каждый раз, когда вы прибегаете к взломам кода символов и манипуляциям со строками для приведения данных в желаемый формат, это становится проблемой.

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

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

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

Благодаря этой новой возможности мы можем переработать предыдущий пример, но на этот раз получить изображение как Blob , а не как строку:

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

xhr.onload = function(e) {
  if (this.status == 200) {
    // Note: .response instead of .responseText
    var blob = new Blob([this.response], {type: '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 можно использовать в нескольких местах, включая сохранение его в indexedDB , запись в файловую систему 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);
}

sendTextNew('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> , вызывая метод добавления.

Конечно, вам не обязательно создавать <form> с нуля. Объекты 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);

Загрузка файла или большого двоичного объекта: xhr.send(Blob)

Мы также можем отправлять данные File или Blob , используя XHR. Имейте в виду, что все File s являются Blob s, поэтому здесь работает любой из них.

В этом примере создается новый текстовый файл с нуля с помощью конструктора Blob() и загружается этот 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);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

Загрузка фрагмента байтов: xhr.send(ArrayBuffer)

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

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);
}

Совместное использование ресурсов между источниками (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 . Один из способов добиться этого — запросить изображения как 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 = 'blob';

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 blob = new Blob([xhr.response], {type: 'image/png'});

        writer.write(blob);

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

xhr.send();

Нарезка файла и загрузка каждой части

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

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) {
    upload(blob.slice(start, end));

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

})();

Здесь не показан код восстановления файла на сервере.

Рекомендации