Neue Tricks in XMLHttpRequest2

Einleitung

XMLHttpRequest ist einer der stillen Helden in HTML5. Genau genommen ist XHR2 kein HTML5. Es gehört jedoch zu den inkrementellen Verbesserungen, die Browseranbieter an der Kernplattform vornehmen. Ich füge auch XHR2 in unser neues Extras ein, weil es in den komplexen Webanwendungen von heute so wichtig ist.

Unser alter Freund wurde komplett überarbeitet, aber vielen Menschen sind die neuen Funktionen nicht bekannt. XMLHttpRequest Level 2 bietet eine Vielzahl neuer Funktionen, die komplizierten Hacks in unseren Webanwendungen ein Ende setzen, z. B. ursprungsübergreifende Anfragen, Hochladen von Fortschrittsereignissen und Unterstützung für das Hoch- und Herunterladen von Binärdaten. Dadurch arbeitet AJAX mit vielen der neuesten HTML5-APIs wie der File System API, der Web Audio API und WebGL zusammen.

In dieser Anleitung werden einige der neuen Features in XMLHttpRequest vorgestellt, insbesondere diejenigen, die für die Arbeit mit Dateien verwendet werden können.

Daten abrufen

Das Abrufen einer Datei als binäres Blob war mit XHR mühsam. Technisch war das nicht einmal möglich. Einer der gut dokumentierten Tricks umfasst das Überschreiben des MIME-Typs mit einem benutzerdefinierten Charset, wie unten dargestellt.

Die alte Methode zum Abrufen von Bildern:

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

Das funktioniert zwar, aber im responseText erhalten Sie kein binäres Blob. Es ist ein binärer String, der die Bilddatei darstellt. Wir überlisten den Server so, dass er die Daten unverarbeitet zurücksendet. Auch wenn dieses kleine Juwel funktioniert, nenne ich es schwarze Magie und berate es dazu. Jedes Mal, wenn Sie Zeichencode-Hacks oder Stringmanipulationen verwenden, um Daten in ein gewünschtes Format zu bringen, ist das ein Problem.

Antwortformat angeben

Im vorherigen Beispiel haben wir das Bild als binäre "Datei" heruntergeladen, indem wir den MIME-Typ des Servers überschrieben und den Antworttext als Binärstring verarbeitet haben. Verwende stattdessen die neuen Attribute responseType und response von XMLHttpRequest, um dem Browser mitzuteilen, in welchem Format die Daten zurückgegeben werden sollen.

xhr.responseType
Bevor Sie eine Anfrage senden, legen Sie xhr.responseType je nach Datenanforderungen auf "text", "arraybuffer", "blob" oder "document" fest. Hinweis: Wenn Sie xhr.responseType = '' festlegen (oder weglassen), wird standardmäßig die Antwort auf „Text“ zurückgegeben.
xhr.response
Nach einer erfolgreichen Anfrage enthält das Antwortattribut von xhr die angeforderten Daten als DOMString, ArrayBuffer, Blob oder Document (je nachdem, was für responseType festgelegt wurde).

Mit dieser neuen Neuerung können wir das vorherige Beispiel überarbeiten. Dieses Mal rufen wir das Bild jedoch als Blob statt als String ab:

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

Viel schöner!

ArrayBuffer-Antworten

Ein ArrayBuffer ist ein generischer Container mit fester Länge für Binärdaten. Sie sind sehr praktisch, wenn Sie einen generalisierten Puffer aus Rohdaten benötigen, aber das Besondere daran ist, dass Sie mit Arrays vom Typ JavaScript „Ansichten“ der zugrunde liegenden Daten erstellen können. Aus einer einzigen ArrayBuffer-Quelle können mehrere Datenansichten erstellt werden. Sie können beispielsweise ein 8-Bit-Integer-Array erstellen, das denselben ArrayBuffer wie ein vorhandenes 32-Bit-Integer-Array aus denselben Daten hat. Die zugrunde liegenden Daten bleiben gleich, wir erstellen nur unterschiedliche Darstellungen.

Im folgenden Beispiel wird dasselbe Bild als ArrayBuffer abgerufen, aber dieses Mal wird ein vorzeichenloser 8-Bit-Ganzzahlarray aus diesem Datenpuffer erstellt:

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-Antworten

Wenn Sie direkt mit einem Blob arbeiten möchten und/oder keine Dateibyte bearbeiten müssen, verwenden Sie 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();

Ein Blob kann an verschiedenen Stellen verwendet werden. Unter anderem können Sie ihn in indexedDB speichern, in das HTML5-Dateisystem schreiben oder eine Blob-URL erstellen (siehe Beispiel).

Daten werden gesendet

Daten in verschiedenen Formaten herunterladen zu können, ist toll, aber das bringt uns nicht weiter, wenn wir diese umfangreichen Formate nicht zurück an die Homebase (den Server) senden können. XMLHttpRequest beschränkt das Senden von DOMString- oder Document-Daten (XML) für einige Zeit. Doch das ist nicht mehr der Fall. Eine überarbeitete Methode send() wurde überschrieben, um einen der folgenden Typen zu akzeptieren: DOMString, Document, FormData, Blob, File, ArrayBuffer. Die Beispiele im Rest dieses Abschnitts zeigen, wie Daten über jeden Typ gesendet werden.

Stringdaten senden: 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');

Es gibt hier nichts Neues, nur das rechte Snippet ist etwas anders. Damit wird responseType='text' für den Vergleich festgelegt. Auch hier führt das Weglassen dieser Linie zu denselben Ergebnissen.

Formulare senden: xhr.send(FormData)

Viele Nutzer sind wahrscheinlich an die Verwendung von jQuery-Plug-ins oder anderen Bibliotheken für das Senden von AJAX-Formularen gewöhnt. Stattdessen können Sie FormData verwenden, einen weiteren neuen Datentyp für XHR2. FormData eignet sich zum schnellen Erstellen eines HTML-<form> in JavaScript. Dieses Formular kann dann mithilfe von AJAX übermittelt werden:

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

Im Wesentlichen erstellen wir einen <form> dynamisch und fügen <input>-Werte hinzu, indem wir die Anfügemethode aufrufen.

Natürlich müssen Sie <form> nicht von Grund auf neu erstellen. FormData-Objekte können aus einer vorhandenen HTMLFormElement auf der Seite initialisiert werden. Beispiel:

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

Ein HTML-Formular kann Dateiuploads enthalten (z.B. <input type="file">) und FormData kann auch diese verarbeiten. Hängen Sie einfach die Datei(en) an und der Browser erstellt eine multipart/form-data-Anfrage, wenn send() aufgerufen wird:

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

Datei oder Blob hochladen: xhr.send(Blob)

Wir können auch File- oder Blob-Daten mit XHR senden. Beachte, dass alle File-Werte Blob sind. Beide funktionieren also hier.

In diesem Beispiel wird mit dem Blob()-Konstruktor eine neue Textdatei erstellt und die Blob wird auf den Server hochgeladen. Mit dem Code wird auch ein Handler eingerichtet, der den Nutzer über den Fortschritt des Uploads informiert:

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

Byte-Chunk hochladen: xhr.send(ArrayBuffer)

Zu guter Letzt können wir noch ArrayBuffers als XHR-Nutzlast senden.

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)

Mit CORS können Webanwendungen in einer Domain domainübergreifende AJAX-Anfragen an eine andere Domain senden. Die Aktivierung ist kinderleicht, denn vom Server muss nur ein einziger Antwortheader gesendet werden.

CORS-Anfragen aktivieren

Angenommen, Ihre Anwendung befindet sich auf example.com und Sie möchten Daten aus www.example2.com abrufen. Wenn Sie versuchen, diese Art AJAX-Aufruf auszuführen, würde die Anfrage normalerweise fehlschlagen, und der Browser würde einen Fehler aufgrund des nicht übereinstimmenden Ursprungs zurückgeben. Mit CORS kann www.example2.com Anfragen von example.com zulassen, indem einfach ein Header hinzugefügt wird:

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

Access-Control-Allow-Origin kann einer einzelnen Ressource innerhalb einer Website oder in der gesamten Domain hinzugefügt werden. Wenn beliebige Domain Anfragen an Sie senden soll, legen Sie Folgendes fest:

Access-Control-Allow-Origin: *

Diese Website (html5rocks.com) hat CORS sogar auf allen Seiten aktiviert. Wenn Sie die Entwicklertools aufrufen, sehen Sie Access-Control-Allow-Origin in unserer Antwort:

Access-Control-Allow-Origin-Header auf html5rocks.com
Header „Access-Control-Allow-Origin“ auf html5rocks.com

Das Aktivieren von ursprungsübergreifenden Anfragen ist einfach. Daher sollten Sie CORS aktivieren, wenn Ihre Daten öffentlich sind.

Domainübergreifende Anfrage stellen

Wenn der Serverendpunkt CORS aktiviert hat, unterscheidet sich die ursprungsübergreifende Anfrage nicht von einer normalen XMLHttpRequest-Anfrage. Hier ist beispielsweise eine Anfrage, die example.com an www.example2.com richten kann:

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

Praktische Beispiele

Dateien im HTML5-Dateisystem herunterladen und speichern

Angenommen, Sie haben eine Bildergalerie und möchten Bilder abrufen und sie dann lokal mit dem HTML5-Dateisystem speichern. Eine Möglichkeit besteht darin, Bilder als Blobs anzufordern und sie mit FileWriter auszuschreiben:

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

Aufteilen einer Datei und Hochladen der einzelnen Teile

Mit den File APIs können wir den Aufwand beim Hochladen einer großen Datei minimieren. Die Technik besteht darin, den Upload in mehrere Blöcke aufzuteilen, eine XHR für jeden Teil zu erzeugen und die Datei auf dem Server zu erstellen. Das funktioniert ähnlich wie Gmail große Anhänge so schnell hochlädt. Mit einer solchen Methode könnte auch das HTTP-Anfragelimit von Google App Engine von 32 MB umgangen werden.

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

})();

Was hier nicht angezeigt wird, ist der Code zur Rekonstruktion der Datei auf dem Server.

Verweise