Nowe triki w XMLHttpRequest2

Wprowadzenie

Jednym z nieznanych bohaterów wszechświata HTML5 jest XMLHttpRequest. Właściwie XHR2 nie jest formatem HTML5. Jest to jednak część stopniowych ulepszeń, które dostawcy przeglądarek wprowadzają w podstawowej platformie. Dodaję XHR2 do naszej nowej paczki z dobrociami, ponieważ odgrywa ono ważną rolę w obecnych złożonych aplikacjach internetowych.

Okazuje się, że nasz stary znajomy przeszedł gruntowną przemianę, ale wiele osób nie zdaje sobie sprawy z jego nowych funkcji. XMLHttpRequest Level 2 wprowadza wiele nowych funkcji, które eliminują konieczność stosowania skomplikowanych obejść. Dotyczy to m.in. żądań między domenami, zdarzeń przesyłania i obsługi przesyłania/pobierania danych binarnych. Dzięki nim AJAX może współpracować z wieloma najnowszymi interfejsami API HTML5, takimi jak File System API, Web Audio API i WebGL.

W tym samouczku omawiamy niektóre nowe funkcje XMLHttpRequest, w szczególności te, których można używać do pracy z plikami.

Pobieram dane

Pobieranie pliku jako binarnego bloba było trudne w przypadku XHR. Z technicznego punktu widzenia było to niemożliwe. Jeden z dobrze udokumentowanych sposobów polega na zastąpieniu typu mime za pomocą zdefiniowanego przez użytkownika zestawu znaków, jak pokazano poniżej.

Stary sposób pobierania obrazu:

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

Chociaż to działa, to w polu responseText nie otrzymasz binarnego bloba. Jest to ciąg binarny reprezentujący plik obrazu. Oszukujemy serwer, aby przekazał z powrotem nieprzetworzone dane. Chociaż ta mała perełka działa, nazywam to czarną magią i nie polecam tego robić. Każdy raz, gdy uciekasz się do stosowania kodów znaków i zmian ciągów znaków, aby wymusić na danych odpowiedni format, jest problemem.

Określanie formatu odpowiedzi

W poprzednim przykładzie obraz został pobrany jako binarny „plik”, aby zastąpić typ mime serwera i przetworzyć tekst odpowiedzi jako ciąg binarny. Zamiast tego użyjemy nowych właściwości XMLHttpRequest responseType i response, aby poinformować przeglądarkę, w jakim formacie mają być zwracane dane.

xhr.responseType
Zanim wyślesz żądanie, ustaw parametr xhr.responseType na „text”, „arraybuffer”, „blob” lub „document” (w zależności od potrzeb). Pamiętaj, że ustawienie wartości xhr.responseType = '' (lub pominięcie) spowoduje, że domyślną odpowiedź będzie stanowić tekst.
xhr.response
Po pomyślnym przesłaniu żądania właściwość odpowiedzi obiektu xhr będzie zawierać żądane dane w postaci obiektu DOMString, ArrayBuffer, Blob lub Document (w zależności od tego, co zostało ustawione dla właściwości responseType).

Dzięki tej nowej funkcji możemy zmodyfikować poprzedni przykład, aby tym razem obraz był pobierany jako Blob zamiast ciągu znaków:

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

znacznie przyjemniej.

Odpowiedzi ArrayBuffer

ArrayBuffer to ogólny kontener o stałyej długości na dane binarne. Są one bardzo przydatne, jeśli potrzebujesz ogólnego bufora danych nieprzetworzonych, ale ich prawdziwa moc polega na tym, że za pomocą tablic typów JavaScripta możesz tworzyć „widoki” danych źródłowych. Z jednego źródła ArrayBuffer można utworzyć wiele widoków. Możesz na przykład utworzyć tablicę liczb całkowitych 8-bitowych, która ma te same wartości ArrayBufferco istniejąca tablica liczb całkowitych 32-bitowych utworzona z tych samych danych. Dane pozostają takie same, ale tworzymy ich różne reprezentacje.

Na przykład poniższy kod pobiera ten sam obraz co ArrayBuffer, ale tym razem tworzy z tego bufora danych tablicę nieoznaczonych 8-bitowych liczb całkowitych:

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

Odpowiedzi blob

Jeśli chcesz pracować bezpośrednio z plikiem Blob lub nie musisz manipulować żadnymi bajtami pliku, użyj 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 może być używany w różnych miejscach, np. do zapisywania w indexedDB, zapisywania w systemie plików HTML5 lub tworzenia adresu URL Blob, jak w tym przykładzie.

Wysyłanie danych

Możliwość pobierania danych w różnych formatach jest świetna, ale nie przyniesie nam żadnych korzyści, jeśli nie będziemy mogli przesłać tych bogatych formatów z powrotem do bazy (serwera). XMLHttpRequest ograniczył nam na jakiś czas możliwość wysyłania danych DOMString lub Document (XML). Już nie. Zaktualizowana metoda send() została zastąpiona, aby mogła akceptować dowolny z tych typów: DOMString, Document, FormData, Blob, File, ArrayBuffer. Przykłady w pozostałej części tego rozdziału pokazują wysyłanie danych za pomocą każdego z tych typów.

Wysyłanie danych ciągu znaków: 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');

Nie ma tu nic nowego, ale kod po prawej stronie jest nieco inny. Ustawia responseType='text' do porównania. Ponownie, pominięcie tej linii daje te same wyniki.

Przesyłanie formularzy: xhr.send(FormData)

Wiele osób prawdopodobnie przyzwyczaiło się do używania wtyczek jQuery lub innych bibliotek do obsługi przesyłania formularzy AJAX. Zamiast tego możemy użyć typu danych FormData, który jest kolejnym nowym typem danych opracowanym na potrzeby XHR2. FormDatajest wygodny do tworzenia w JavaScriptie kodu HTML <form> na bieżąco. Formularz można następnie przesłać za pomocą 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);
}

W podstawie tworzymy dynamicznie obiekt <form> i dodajemy do niego wartości <input>, wywołując metodę append.

Oczywiście nie musisz tworzyć <form> od podstaw. Obiekty FormData można inicjować na podstawie istniejących obiektów HTMLFormElement na stronie. Na przykład:

<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.
}

Formularz HTML może zawierać przesyłanie plików (np. <input type="file">), a FormData też to obsługuje. Po prostu dołącz pliki, a przeglądarka utworzy żądanie multipart/form-data, gdy wywołane zostanie 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);

Przesyłanie pliku lub bloba: xhr.send(Blob)

Możemy też wysyłać dane File lub Blob za pomocą XHR. Pamiętaj, że wszystkie File to Blob, więc obie opcje są prawidłowe.

W tym przykładzie nowy plik tekstowy jest tworzony od podstaw za pomocą konstruktora Blob(), a następnie przesyłany na serwer.Blob Kod tworzy też moduł obsługi, aby informować użytkownika o postępie przesyłania:

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

Przesyłanie fragmentu bajtów: xhr.send(ArrayBuffer)

Na koniec możemy wysłać ArrayBuffer jako dane ładunku 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);
}

współdzielenie zasobów pomiędzy serwerami z różnych domen (CORS);

CORS umożliwia aplikacjom internetowym w jednej domenie wysyłanie żądań AJAX do innej domeny. Włączenie tej funkcji jest bardzo proste i wymaga tylko wysłania przez serwer jednego nagłówka odpowiedzi.

Włączanie żądań CORS

Załóżmy, że Twoja aplikacja działa na platformie example.com i chcesz pobrać dane z domeny www.example2.com. Jeśli spróbujesz wykonać tego typu wywołanie AJAX, żądanie nie powiedzie się, a przeglądarka wyświetli błąd niezgodności pochodzenia. Dzięki CORS usługa www.example2.commoże zezwalać na żądania z domeny example.com, po prostu dodając nagłówek:

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

Access-Control-Allow-Origin można dodać do pojedynczego zasobu w ramach witryny lub całej domeny. Aby dowolna domena mogła wysłać do Ciebie żądanie, ustaw:

Access-Control-Allow-Origin: *

W rzeczywistości ta witryna (html5rocks.com) ma włączony mechanizm CORS na wszystkich swoich stronach. Uruchom narzędzia dla deweloperów. W odpowiedzi zobaczysz Access-Control-Allow-Origin:

Nagłówek Access-Control-Allow-Origin na html5rocks.com
Nagłówek „Access-Control-Allow-Origin” na html5rocks.com

Włączanie żądań między domenami jest proste, dlatego prosimy o włączenie CORS, jeśli Twoje dane są publiczne.

Wysyłanie żądania w wielu domenach

Jeśli punkt końcowy serwera ma włączoną obsługę CORS, wysłanie żądania między domenami nie różni się od zwykłego żądania XMLHttpRequest. Oto przykład żądania, które example.com może teraz wysłać do 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();

Praktyczne przykłady

Pobieranie i zapisywanie plików w systemie plików HTML5

Załóżmy, że masz galerię obrazów i chcesz pobrać kilka obrazów, a potem zapisać je lokalnie za pomocą systemu plików HTML5. Jednym ze sposobów jest wysłanie żądania obrazów jako Blobi zapisanie ich za pomocą 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();

dzielenie pliku na części i przesyłanie poszczególnych części,

Dzięki interfejsom File API możemy zminimalizować wysiłek związany z przesyłaniem dużych plików. Polega ona na podzieleniu przesyłanego pliku na kilka części, uruchomieniu dla każdej z nich wywołania XHR i złożeniu pliku na serwerze. Jest to podobne do tego, jak Gmail szybko przesyła duże załączniki. Ta technika może też służyć do obejścia limitu żądań HTTP wynoszącego 32 MB w Google App Engine.

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

})();

Nie pokazano tutaj kodu służącego do odtworzenia pliku na serwerze.

Pliki referencyjne