Пользователь
0,0
рейтинг
8 июня 2011 в 19:07

Разработка → Новые возможности XMLHttpRequest2 перевод

Одним из незамеченных героев вселенной 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
Перевод: Eric Bidelman
Mikhail Davydov @azproduction
карма
448,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (32)

  • +2
    Действительно много интересного добавили.
    Давно уже хочу такую штуку чтобы сделали, возможность дозагрузки файла на сервер, в случае обрыва.
    зашел в свой интрефейс а там запись что файл загружали, и возможность дозалить его на сервер, я думаю что обменник с таким сервисом давно бы всех победил.
    • +1
      Вот вы сами только что произнесли годную идею для стартапа: файлообменник с возможность дозаливки при обрыве. Если технически реализуете эту идею быстрее чем другие — получите профит.

      • +2
        В юзкейсах спецификаций w3с очень много идей для старапов — советую почитать ;) Вот, например, юзкейсы FileSystem API
        • 0
          Идеи-то конечно есть, я говорю о том, но пока еще нет сервисов, которые эти идеи реализовывали бы в качестве «killer feature» пока нет. Так что, спасибо за хорошую статью — будет над чем подумать.
          • 0
            Дозагрузка через FileAPI и Silverlight уже достаточно давно работает у нас на files.mail.ru
            Правда для инициации дозагрузки в случае обрыва и т.п. всё-таки надо повторно выбрать файл в диалоге.
    • +1
      В спецификации FileSystem API даже есть такой юзкейс:
      Persistent uploader
      — When a file or directory is selected for upload, it copies it into a local sandbox and uploads a chunk at a time.
      — It can restart uploads after browser crashes, network interruptions, etc.

      1. Мы можем скопировать файл в хранилище FileSystem API и дозагружать из него — пользователю не нужно повторно выбирать файл, мы уверены, что файл не изменится
      2. У XHR2 есть событие «progress» в котором сообщается сколько байт файла уже загрузилось — можем узнать сколько уже загрузилось
      3. Мы можем делить файлы на более мелкие (Пример 2: Отправка файла по частям) — можем именно дозагружать
      4. У нас есть localStorage — можем сохранять процесс аплоада файла и восстановить его после обрыва, закрытия окна
  • +3
    которые положат конец нашим безумным хакам и пляскам с бубном

    Пока живы ИЕ 6-8, не положат:(
    • +3
      Пока разработчики не положат на ИЕ 6-8, они будут жить.
      • +3
        Иногда проще написать под ИЕ6, чем потом мучаться с пользователями ИЕ:)
      • +1
        разработчики ведь не сами формируют требования. этим занимается заказчик.
    • +3
      Ну так-то да, пока еще это использовать массово нельзя. Но кто мешает использовать не массово? Те же FormData, DragDrop и DataTransfer сильно облегчают мне жизнь при написании, например, админок. Просто я говорю клиенту, что админка будет работать только в FF4 и Chrome — никто пока не возражал.
    • +1
      Для ie8 есть XDomainRequest
      В связке с XMLHTTPRequest 2, работает начиная с Firefox 3.5/Safari 4/IE8.

      Пример и пояснения можно посмотреть тут.
      • 0
        В IE8-9 и правда есть XDomainRequest, но там не все так хорошо как кажется. Во первых нельзя передавать свои хедеры, вообще. во вторых они даже Content-Type посчитали несекьюрным, так что на серверную сторону запросы приходят без него и приходится rewriterul'ами его добавлять чтобы серверная сторона начала распарссивать форму в пост данных. С http referer тоже проблемы. Так что пока IE не реализует level2 вокруг него еще плясать и плясать с бубном…
  • 0
    Респект!
    Утащил к себе. Спасибо за инфу )
  • +1
    А вообще, конечно, всё это — HTML5, CSS3, всякие новые вкусные API — очень радует. Вспоминая, что и как приходилось городить 2, 5, 8 лет назад — так сейчас просто рай для разработчика…
  • 0
    Заголовок Access-Control-Allow-Origin может быть выдан одному сайту или целому поддомену или даже любому сайту с любого домена

    Как разрешить запросы для нескольких выбранных доменов, например example.com и anotherexample.com? Только без предположений и гаданий на кофейной гуще, а рабочий способ, пожалуйста.
    • 0
      <?php
      // можно ограничить домен, для которого доступен ответ
      header('Access-Control-Allow-Origin: javascript.ru');
      ?>

      The Access-Control-Allow-Origin header should contain a comma separated list of acceptable domains. (документация)
      • 0
        А вы пробовали так сделать, работает?
        • 0
          Задача так не стояла — не пробовал,
          хотя натыкался на проблему не работоспособности.

          Предлагаемые решения были такими:
          1. На клиенте считать Origin header, сравнить с теми что сами разрешаем, и отдать только разрешенный адрес в Access-Control-Allow-Origin
          2. Отправить вот так:
          Access-Control-Allow-Origin: domain1.com
          Access-Control-Allow-Origin: domain2.com
          Access-Control-Allow-Origin: domain3.com

          Для апача с использование mod_headers в .htaccess
          Header add Access-Control-Allow-Origin "http://domain1.com"
          Header add Access-Control-Allow-Origin "http://domain2.com"
          Header add Access-Control-Allow-Origin "http://domain3.com"


          Для любознательных спецификация.

          *ради интереса надо попробовать.
          • 0
            Вот и у меня, когда я пробовал (правда было это наверно полгода назад) как-то не заработало оно…
            • 0
              Специально проверил. Firefox понимает только формат Access-Control-Allow-Origin: origin | * ни списки ни многозаголовочность не понимает.
              Работает вот так:
              Access-Control-Allow-Origin: http://bla-lba.ru
              или так:
              Access-Control-Allow-Origin: *
              • 0
                Т.е. насколько я понимаю пока самый кроссбраузерный вариант — это

                1. На клиенте считать Origin header, сравнить с теми что сами разрешаем, и отдать только разрешенный адрес в Access-Control-Allow-Origin
  • 0
    Я все правильно понимаю, кроссдоменный запрос придет в любом случае, просто клиент может не получить на него ответ? И при этом все куки передаются?
    • +2
      Необходимо выставить флаг withCredentials — по умолчанию данные пользователя (Cookie, Authorization, клиентские сертификаты) не передаются.
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'http://example.com/');
      xhr.withCredentials = true; // <<<
      

      Чтобы все работало сервер должен выставить заголовок Access-Control-Allow-Credentials: true
      • 0
        Если это так, то это небезопасно, теперь можно проводить CSRF-атаки вообще без ведома пользователя. Сервер такой запрос все равно получит и обработает, вне зависимости от наличия нужных заголовков в ответе
        • 0
          Ничто не мешает это делать и без XHR2. Создаём динамически форму и отправляем её в скрытый iframe.
          • 0
            Насколько помню, в ифреймы с других доменов куки не передаются. Это поведение настраивается у многих браузеров в настройках безопасности, и по умолчанию именно такое. Хотя последний раз я с этим сталкивался довольно давно, возможно, сейчас дела обстоят иначе
  • 0
    Маленько не понимаю почему именно так работает. Логичнее было бы ограничивать ресурсы на стороне сервера который обслуживает клиент, а не там куда надо сделать запрос (ну или оба). Так я много чего не хорошего сделать в случае если попадется ресурс где можно заинжектит JS код.
    • 0
      точно, странное решение, ничего не дающее. опечатка?
      • 0
        Неа, я еще когда черновики только появились задался этим вопросом.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    В статье мелькает свойство upload xhr объекта, которое, между тем, является полезным при мониторинге прогресса запроса.
    Это самое свойство — ссылка на соответствующий экземпляр XMLHttpRequestUpload, которому можно назначить обработчики на следующие события: onloadstart, onprogress, onabort, onerror, onload, ontimeout, onloadend.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.