Möchte man eine Datei per AJAX an einen Server senden, kann man das mit einem XMLHttpRequest tun. In früheren Versionen wurde selbiges nicht vorgesehen, weswegen oftmals mit IFrames gearbeitet wurde; das ist jetzt aber dank des Interface FormData() nicht mehr nötig¹.
Dieses Interface ermöglicht ein BLOB (binary large object) mit dem Request mitzusenden. Die theoretischen Fakten kann man auf der verlinkten Seite des w3c nachlesen, wir gehen zur Praxis. Übrigens nutze ich für derartige Sachen jQuery. Die damit verbundene Vereinfachung wiegt den zusätzlich nötigen Traffic für die Library bei weitem auf.

Der einfachste Fall

Wir haben ein Input-Element, und immer, wenn sich das ändert, wird eine Datei hochgeladen.

HTML:

    <title>Upload per AJAX</title>

      <input type="file" id="uploadFile">
      <div id="responses"></div>

JS:

    // Wir registrieren einen EventHandler für unser Input-Element (#uploadFile)
    // wenn es sich ändert
    $('body').on('change', '#uploadFile', function() {
       var data = new FormData(); // das ist unser Daten-Objekt ...
       data.append('file', this.files[0]); // ... an die wir unsere Datei anhängen
       $.ajax({
          url: 'myscript.php', // Wohin soll die Datei geschickt werden?
          data: data,          // Das ist unser Datenobjekt.
          type: 'POST',         // HTTP-Methode, hier: POST
          processData: false,
          contentType: false,
          // und wenn alles erfolgreich verlaufen ist, schreibe eine Meldung
          // in das Response-Div
          success: function() { $("#responses").append("
             Datei erfolgreich hochgeladen");
          }
       });
    }

PHP (myscript.php)

    <?php
       if(is_writable(".") && isset($_FILES['file'])) {
          move_uploaded_file($_FILES['file']['name'], ".");
       }
    ?>

Der Upload mit der jQuery-Funktion $.ajax() bleibt auch bei anderen Konstellationen gleich. Zu Beachten ist nur, dass ein jQuery-Objekt nicht direkt die Eigenschaft „files“ hat, sondern entweder das Input-Element mit herkömmlichen Javascript-Mitteln angesprochen werden muss

data.append('file', document.getElementById('uploadFile').files[0]);

oder das Element über den jQuery-Selektor präzisiert werden muss

data.append('file', $("#uploadFile")[0].files[0]);

Und fertig. Der AJAX-Upload sollte funktionieren. Wenn jemandem ein Fehler aufgefallen ist, darf er das gerne kommentieren.

Update: Fortschrittsanzeige

Wenn man einen File-Upload per AJAX innerhalb einer Webanwendung einsetzt, kann man nebenbei weiter arbeiten. In diesem Fall ist es sinnvoll zu sehen, wie lange der Upload noch dauert bzw. wie weit der Upload schon fortgeschritten ist. In meinem „Improved File Manager“ habe ich diese Funktion auch verwendet. Hier ist der Code etwas abgeändert:

// Datei, die hochgeladen werden soll
var upload_file = document.getElementById('upload_file').files[0];
// Daten, die mitgesendet werden sollen
var data = new FormData();
data.append('parameter', 'wert'); // es können viele weitere Parameter definiert werden
data.append('file', upload_file); // dieser Parameter kann auch anders heißen

// AJAX Request
$.ajax({
    url: "http://example.com/upload.php",
    type: "POST",
    data: data,
    processData: false,
    contentType: false,
    dataType: "json", // in meinem Fall JSON, da ich ein JSON-Objekt mit einigen Infos zurückgebe
    xhr: function(){
        var xhr = $.ajaxSettings.xhr() ;
        xhr.upload.onprogress = function(evt){ /* Hier kann man die Fortschrittsanzeige updaten. Beispielsweise prozent = (evt.loaded/evt.total*100); */ } ;
        xhr.upload.onload = function(){ console.log('Upload done.') } ;
        return xhr ;
    },
    success: function(response) {
       console.log(response.status); // dafür muss man natürlich einen Status zurückgeben. Andernfalls tut man eben nichts.
    },
    error: function() { console.log("General error occured", "e"); },
    complete: function() { /* Fertig. */}
});

Wichtig ist hierbei die xhr-Eigenschaft. Wie man sieht wird innerhalb der Funktion ein Objekt erstellt, und Callbacks definiert. Die Funktion hinter onprogress berechnet den prozentualen Fortschritt und loggt diesen in die Console. Alternativ kann man natürlich auch das DOM verändern und dem User direkt den Fortschritt mitteilen.  Dafür bietet sich beispielsweise eine Progressbar an, die jQuery UI anbietet. Das Skript, welches den Upload serverseitig verarbeitet sollte auch zurückgeben, ob der Upload funktioniert hat; es kann ja sein, dass das Dateisystem temporär nicht beschreibbar ist, oder ein anderer Fehler auftritt. Ich gebe meine Infos meistens als JSON zurück. Das könnte dann so aussehen:

{
  "status": "OK",
  "message": "File successfully uploaded."
}

Wenn der Upload serverseitig nicht geklappt hat, wird natürlich der Status auf ERROR gesetzt und eine entsprechende Fehler-Message mitgeliefert.

[1] Anmerkung: Es gab da ein working draft über einen XMLHttpRequest Level 2, welcher aber obsolet markiert wurde. Offensichtlich hat das Interface FormData() es in eine neuere Version des XMLHttpRequest Level 1 geschafft. Den gesamten Hintergrund der Ereignisse kenne ich nicht, wenn jemand mehr Informationen dazu hat,  würde ich mich über eine Erklärung freuen 🙂

Nächster Beitrag Vorheriger Beitrag