Nowe triki w XMLHttpRequest2

Wstęp

Jedna z bohaterek świata HTML5 to XMLHttpRequest. XHR2 to nie HTML5. Jest to jednak część stopniowych ulepszeń wprowadzanych przez dostawców przeglądarek w podstawowej platformie. Dlatego właśnie XHR2 znajduje się w naszej nowej torbie ulubionych funkcji, ponieważ jest nieodłącznym elementem współczesnych złożonych aplikacji internetowych.

Wygląda na to, że nasz stary znajomy przeszedł metamorfozę, ale wiele osób nie wie o nowych funkcjach. XMLHttpRequest Level 2 wprowadza wiele nowych funkcji, które pozbywają się skomplikowanych ataków na nasze aplikacje internetowe, takich jak żądania z innych domen, przesyłanie zdarzeń postępu czy obsługa przesyłania i pobierania danych binarnych. Pozwalają one działać w połączeniu z wieloma najnowszymi interfejsami API HTML5, takimi jak File System API, Web Audio API i Web Audio.

W tym samouczku omawiamy niektóre nowe funkcje w XMLHttpRequest, zwłaszcza te, których można używać do pracy z plikami.

Pobieram dane

W przypadku XHR pobieranie pliku jako binarnego obiektu blob było kłopotliwe. Z technicznego punktu widzenia nie było to w ogóle możliwe. Jedna z trików, które zostały dobrze udokumentowane, polega na zastąpieniu typu MIME za pomocą zestawu znaków zdefiniowanego przez użytkownika, jak pokazano poniżej.

Stary sposób pobierania zdjęcia:

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

Mimo że to działa, w responseText otrzymasz nie binarny blob. Jest to ciąg binarny reprezentujący plik graficzny. Podstępem nakłaniamy serwer, by przekazał dane z powrotem, w nieprzetworzonych celach. Chociaż ten mały klejnot działa, nazwam go „czarną magią” i mu radzę. Za każdym razem, gdy używasz hakowania kodu znaków i manipulacji ciągami znaków w celu przekształcenia danych do pożądanego formatu, jest to problem.

Określanie formatu odpowiedzi

W poprzednim przykładzie pobraliśmy obraz jako binarny „plik” przez zastąpienie typu MIME serwera i przetworzenie tekstu odpowiedzi jako ciągu binarnego. Wykorzystajmy nowe właściwości responseType i response w XMLHttpRequest, aby poinformować przeglądarkę, w jakim formacie mają być zwracane dane.

xhr.responseType
Przed wysłaniem żądania ustaw xhr.responseType na „text”, „arraybuffer”, „blob” lub „document” (w zależności od potrzeb). Pamiętaj, że ustawienie xhr.responseType = '' (lub jego pominięcie) spowoduje domyślną odpowiedź na „tekst”.
xhr.response
Gdy żądanie zostanie zrealizowane, właściwość odpowiedzi xhr będzie zawierać żądane dane w postaci DOMString, ArrayBuffer, Blob lub Document (w zależności od ustawień responseType).

Dzięki tym nowościom możemy przerobić poprzedni przykład, ale tym razem pobierz obraz jako element 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();

Dużo ładniej!

Odpowiedzi z bufora tablicy

ArrayBuffer to ogólny kontener o stałej długości na dane binarne. Są one bardzo przydatne, jeśli potrzebujesz uogólnionego bufora nieprzetworzonych danych. Ich prawdziwą mocą jest to, że możesz tworzyć „widoki” danych bazowych za pomocą tablic z danymi JavaScript. Na podstawie jednego źródła danych ArrayBuffer można utworzyć wiele widoków danych. Możesz na przykład utworzyć 8-bitową tablicę liczb całkowitych z tą samą wartością parametru ArrayBuffer co istniejąca 32-bitowa tablica liczb całkowitych z tych samych danych. Dane bazowe pozostają takie same, tylko tworzymy ich różne reprezentacje.

Na przykład ten kod pobiera ten sam obraz co element ArrayBuffer, ale tym razem tworzy nieoznaczoną 8-bitową tablicę liczb całkowitych z tego bufora danych:

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 Blob i nie musisz modyfikować żadnych bajtów 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();

Elementu Blob można używać w wielu miejscach, m.in. do zapisania w indexedDB, zapisywania w systemie plików HTML5 lub tworzenia adresu URL obiektu Blob, jak widać w tym przykładzie.

Wysyłanie danych

Możliwość pobierania danych w różnych formatach to świetna sprawa, ale nic nie zaszkodzi, jeśli nie uda nam się wysłać tych formatów do głównej bazy danych (na serwer). XMLHttpRequest przez jakiś czas ograniczał się do wysyłania danych DOMString lub Document (XML). Już nie. Ulepszona metoda send() została zastąpiona, aby akceptować te typy: DOMString, Document, FormData, Blob, File, ArrayBuffer. W pozostałej części tej sekcji znajdziesz przykłady wysyłania danych za pomocą poszczególnych typów.

Wysyłanie danych ciągu: 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 niczego nowego, ale prawy fragment jest nieco inny. Ustawia wartość responseType='text' na potrzeby porównania. Jak już wspomniano, pominięcie tej linii da takie same wyniki.

Przesyłanie formularzy: xhr.send(FormData)

Wiele osób jest przyzwyczajonych do obsługi przesyłania formularzy w technologii AJAX za pomocą wtyczek jQuery lub innych bibliotek. Zamiast niego możemy użyć FormData, kolejnego nowego typu danych opracowanego dla XHR2. FormData to wygodny sposób na błyskawiczne tworzenie kodu <form> HTML w języku JavaScript. Formularz ten można następnie przesłać za pomocą technologii 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);
}

Zasadniczo po prostu dynamicznie tworzymy obiekt <form> i dopasowujemy do niego wartości <input>, wywołując metodę dołączania.

Oczywiście nie musisz tworzyć <form> od podstaw. Obiekty FormData można zainicjować na podstawie elementów HTMLFormElement znajdujących się 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 obsługiwać przesyłane pliki (np.<input type="file">). Formularz FormData obsługuje też przesyłanie takich plików. Wystarczy, że dołączysz pliki, a przeglądarka utworzy żądanie multipart/form-data po wywołaniu funkcji 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 obiektu blob: xhr.send(Blob)

Możemy też wysyłać dane z aplikacji File lub Blob za pomocą XHR. Pamiętaj, że wszystkie komponenty File to Blob, więc każda z nich działa tutaj.

W tym przykładzie tworzymy od podstaw nowy plik tekstowy za pomocą konstruktora Blob() i przesyłamy ten plik Blob na serwer. Kod konfiguruje też moduł obsługi, który informuje 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)

Jako ładunek XHR możemy też wysłać 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);
}

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 jednego nagłówka odpowiedzi przez serwer.

Włączam żądania CORS

Załóżmy, że Twoja aplikacja działa w usłudze example.com i chcesz pobierać dane z serwera www.example2.com. Normalnie przy próbie wykonania tego typu wywołania AJAX żądanie nie powiedzie się, a przeglądarka zgłosi błąd niezgodności źródła. Dzięki CORS www.example2.com może zezwolić na żądania z example.com. Wystarczy dodać nagłówek:

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

Kafelek Access-Control-Allow-Origin można dodać do jednego zasobu w witrynie lub w całej domenie. Aby zezwolić dowolnej domenie na wysyłanie żądań do Ciebie, ustaw:

Access-Control-Allow-Origin: *

Ta witryna (html5rocks.com) włączyła CORS na wszystkich swoich stronach. Gdy uruchomisz Narzędzia dla programistów, zobaczysz Access-Control-Allow-Origin w naszej odpowiedzi:

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

Włączanie żądań z innych domen jest proste, dlatego włącz CORS, jeśli Twoje dane są publiczne.

Wysyłanie żądania między domenami

Jeśli punkt końcowy serwera ma włączony CORS, wysyłanie żądania z innych domen nie różni się od zwykłego żądania XMLHttpRequest. Oto na przykład żądanie, 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 następnie zapisać je lokalnie za pomocą systemu plików HTML5. Możesz to zrobić, wysyłając żądanie obrazów w formacie Blob i zapisywanie ich za pomocą funkcji 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();

Wycinanie pliku i przesyłanie jego części

Za pomocą interfejsów API plików możemy zminimalizować nakład pracy związany z przesyłaniem dużych plików. Polega ona na podzieleniu przesyłanego pliku na kilka fragmentów, wygenerowaniu po jednym XHR dla każdej części i umieszczeniu pliku na serwerze. Działa to podobnie do Gmaila, który przesyła duże załączniki tak szybko. W ten sposób można też obejść limit żądań http dla Google App Engine wynoszący 32 MB.

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 widać tu kodu do zrekonstruowania pliku na serwerze.

Źródła