Neue Tricks für XMLHttpRequest2

HTML5 Rocks

Einführung

Einer der stillen Helden in der HTML5-Welt ist XMLHttpRequest. Wenn man es genau nimmt, ist XHR2 kein HTML5. Es gehört jedoch zu den inkrementellen Verbesserungen, die Browserhersteller an der Kernplattform vornehmen. Ich schließe XHR2 in unsere neue "Wundertüte" ein, weil es in den komplexen Webanwendungen von heute so eine wichtige Rolle spielt.

Sie werden sehen, dass hier viele Überarbeitungen vorgenommen wurden. Die neuen Funktionen sind vielen Leuten aber gar nicht bekannt. XMLHttpRequest Level 2 enthält eine Menge neue Möglichkeiten, durch die verrückte Hacks bei unseren Webanwendungen endlich überflüssig werden. Dazu gehören Dinge, wie ursprungsübergreifende Anforderungen, das Hochladen von Fortschrittsereignissen und eine Unterstützung für das Hochladen/Herunterladen von Binärdaten. Dadurch funktioniert AJAX gemeinsam mit vielen der führenden HTML5-APIs, wie dem File System API, Web Audio API und WebGL.

In dieser Anleitung werden einige der neuen Funktionen in XMLHttpRequest erläutert, vor allem diejenigen, die für die Arbeit mit Dateien verwendet werden können.

Daten abrufen

Mit XHR war es schwierig eine Datei als binären Blob abzurufen. Technisch war es nicht einmal möglich. Bei einem sehr gut beschriebenen Trick musste der MIME-Typ wie folgt mit einem nutzerdefinierten "charset" aufgehoben werden.

Die bisherige Möglichkeit zum Abrufen eines Image:

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 in responseText erhalten Sie keinen binären Blob. Es ist ein binärer String, der die Image-Datei repräsentiert. Wir überlisten den Server, so dass er die Daten unverarbeitet zurücksendet. Dieser Trick funktioniert zwar, dennoch finde ich diese Vorgehensweise nicht ideal und rate davon ab. Immer wenn man Zeichencode-Hacks und String-Manipulationen verwendet, um Daten in das gewünschte Format zu bekommen kommt es auch zu Problemen.

Antwortformat angeben

Im vorigen Beispiel wurde das Image als Binärdatei heruntergeladen, indem der MIME-Typ des Servers übergangen und der Antworttext als Binärstring verarbeitet wurde. Nutzen wir stattdessen die neuen Eigenschaften responseType und response von XMLHttpRequest, um dem Browser mitzuteilen, in welchem Format die Daten zurückgegeben werden sollen.

xhr.responseType
Vor dem Senden einer Anforderung, setzen Sie xhr.responseType auf "text", "arraybuffer", "blob" oder "document", je nach Ihren Datenanforderungen. Hinweis: Durch das Festlegen von xhr.responseType = '' oder ohne Eintrag lautet die Standardeinstellung für die Antwort auf "text".
xhr.response
Nach einer erfolgreichen Anforderung enthält die Antworteigenschaft von xhr die angeforderten Daten als DOMString, ArrayBuffer, Blob oder document, abhängig von der Einstellung für responseType.

Mit dieser neuen, tollen Möglichkeit können wir das vorige Beispiel überarbeiten und dieses Mal das Image als ArrayBuffer, statt als String, abfragen. Durch das Weiterreichen des Puffers an das BlobBuilder-API wird ein Blob erstellt:

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); // Note: not xhr.responseText

    var blob = bb.getBlob('image/png');
    ...
  }
};

xhr.send();

Das ist viel besser!

ArrayBuffer-Antworten

Ein ArrayBuffer ist ein generischer Container mit fester Länge für Binärdaten. Diese sind sehr praktisch, wenn Sie einen generalisierten Puffer mit Rohdaten möchten, aber das Besondere daran ist, dass Sie "Ansichten" der zugrunde liegenden Daten erstellen können. Dazu verwenden Sie Arrays mit dem Typ JavaScript . Tatsächlich können aus einer einzelnen ArrayBuffer-Quelle mehrere Ansichten erstellt werden. Beispielsweise könnten Sie einen 8-Bit-Integer-Array erstellen, der denselben ArrayBuffer wie ein vorhandener 32-Bit-Integer-Array aus denselben Daten verwendet. Die zugrunde liegenden Daten bleiben dieselben, wir erstellen lediglich unterschiedliche Darstellungen.

Beispiel: Durch den folgenden Code wird dasselbe Image als ArrayBuffer abgerufen, aber hier wird ein vorzeichenloser 8-Bit-Integer-Array von 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 Dateibytes manipulieren müssen, sollten Sie xhr.responseType='blob' verwenden:

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 vielen Orten verwendet werden. Dazu gehört die Speicherung in indexedDB, das Schreiben im HTML5-Dateisystem oder das Erstellen einer Blob-URL, wie in diesem Beispiel gezeigt.

Daten senden

Die Möglichkeit zum Herunterladen von Daten in verschiedenen Formaten ist toll, aber es hilft nichts, wenn sich diese umfangreichen Formate nicht zurück zum Server senden lassen. XMLHttpRequest beschränkte das Senden einige Zeit lang auf DOMString oder Document (XML)-Daten. Das ist nicht mehr der Fall. Eine neue send()-Methode wird übergangen, 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.

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

sendText2('test string');

Es gibt hier nichts Neues, obwohl das rechte Snippet leicht anders ist. Es legt responseType='text' für den Vergleich fest. Auch hier gilt: Das Auslassen dieser Zeile bringt dieselben Ergebnisse.

Formulare übermitteln: xhr.send(FormData)

Viele Nutzer sind wahrscheinlich an die Verwendung von jQuery plugins oder anderen Bibliotheken gewöhnt, um AJAX-Formularübermittlungen zu verwalten. Stattdessen können Sie auch FormData nutzen. Hierbei handelt es sich um einen neuen Datentyp für XHR2. FormData ist beim schnellen Erstellen eines HTML-<form> in JavaScript hilfreich. Dieses Formular kann mit 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);
}

Eigentlich erstellen Sie lediglich dynamisch ein <form> und fügen <input>-Werte hinzu, indem Sie die "append"-Methode aufrufen.

Sie müssen natürlich kein <form> von Grund auf erstellen. FormData-Objekte können von einem vorhandenen HTMLFormElement auf der Seite initialisiert werden. Zum 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 Datei-Uploads enthalten (z. B. <input type="file">) und FormData kann auch diese verarbeiten. Hängen Sie die Datei(en) einfach an und der Browser wird eine multipart/form-data-Anforderung erstellen, sobald 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)

Sie können mithilfe von XHR auch File oder Blob-Daten übermitteln. Beachten Sie, dass alle Files Blobs sind. Daher funktioniert hier beides.

In diesem Beispiel wird eine Textdatei anhand dem BlobBuilder-API von Grund auf neu erstellt und dieser Blob wird auf den Server geladen. Der Code richtet auch einen Handler ein, der den Nutzer über den Upload-Vorgang 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);
}

// Take care of vendor prefixes.
BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

var bb = new BlobBuilder();
bb.append('hello world');

upload(bb.getBlob('text/plain'));

Byte-Chunks hochladen: xhr.send(ArrayBuffer)

Nicht zuletzt lassen sich auch 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 übergreifende AJAX-Anforderungen an eine andere Domain stellen. Das lässt sich sehr leicht aktivieren und erfordert nur, dass der Server einen einzelnen Antwort-Header sendet.

CORS-Anforderungen aktivieren

Nehmen wir einmal an, dass sich Ihre Anwendung auf example.com befindet und dass Sie Daten von www.example2.com abrufen möchten. Wenn Sie für gewöhnlich diese Art AJAX-Abrufe vornehmen, würde die Anforderung fehlschlagen. Der Browser gäbe einen Fehler in Bezug auf die fehlende Ursprungsübereinstimmung aus. Mit CORS kann www.example2.com Anforderungen von example.com zulassen, indem ganz einfach ein Header hinzugefügt wird:

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

Access-Control-Allow-Origin kann für eine einfache Ressource in einer Website oder für die gesamte Domain hinzugefügt werden. Wenn Sie zulassen möchten, dass jede beliebige Domain Ihnen eine Anforderung senden darf, nehmen Sie folgende Einstellungen vor:

Access-Control-Allow-Origin: *

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

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

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

Domain-übergreifende Anforderung erstellen

Wenn der Server-Endpunkt CORS aktiviert hat, wird eine der ursprungsübergreifende Anforderung genauso gestellt, wie eine gewöhnliche XMLHttpRequest-Anforderung. Hier sehen Sie eine Anforderung, die example.com nun www.example2.com stellen 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();

Praxisbeispiele

Dateien im HTML5-Dateisystem herunterladen und speichern

Nehmen wir an, Sie haben eine Bildergalerie und speichern diese lokal mit dem HTML5-Dateisystem. Das ist möglich, indem Sie Bilder als ArrayBuffer anfordern, einen Blob aus den Daten erstellen und den Blob mithilfe von FileWriter speichern:

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

        var bb = new BlobBuilder();
        bb.append(xhr.response);

        writer.write(bb.getBlob('image/png'));

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

xhr.send();

Hinweis: Zur Verwendung dieses Codes sollten Sie die Anmerkungen zur Browserunterstützung und zur Speicherbeschränkung in der Anleitung FileSystem APIs erkunden beachten.

Datei teilen und Teile hochladen

Unter Verwendung der File APIs können Sie den Arbeitsaufwand beim Upload einer großen Datei optimieren. Dabei wird die Upload-Datei in mehrere Teile aufgeteilt. Für jeden Teil wird eine XHR erzeugt und die Datei wird auf dem Server zusammengesetzt. So ähnlich geht auch Google Mail beim schnellen Upload großer Dateianhänge vor. Mit dieser Vorgehensweise könnte man auch die Anforderungsbeschränkung der Google App Engine von 32 MB umgehen.

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 chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {

    // Note: blob.slice has changed semantics and been prefixed. See 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);

})();

Der Code für den Zusammenbau der Datei auf dem Server wird hier nicht angezeigt.

Probieren Sie es aus!

#bytes/chunk:

Referenzen

Comments

0