Εισαγωγη

Ένα από τα πιο σημαντικά στοιχεία αναβάθμισης της HTML5 είναι το XMLHttpRequest. Τυπικά, XHR2 δεν είναι HTML5. Παρολαυτά, αποτελεί μια από τις βασικές βελτιώσεις που έχουν κάνει οι browser vendors στους Browsers.

Το XMLHttpRequest Level 2 εισάγει πλήθος νέων δυνατοτήτων που απλοποιούν της εφαρμογές που είναι βασισμένες στο web. Μερικές από αυτές είναι πολύ σημαντικές, όπως: cross-origin requests - CORS, uploading progress events και υποστήριξη για uploading/downloading binary δεδομένων. Αυτές επιτρέπουν στις AJAX εφαρμογές να συνεργάζονται με άλλα χρήσιμα API της HTML5 όπως το File System API, Web Audio API, και WebGL.

Αυτό το tutorial αναδεικνύει κάποια από τις καινοτομίες του XMLHttpRequest, ιδιαίτερα εκείνες που μπορούν να χρησιμοποιηθούν για επεξεργασία αρχείων.

Ανακτηση δεδομενων απο τον server

οριζοντας το response format

Ας εξετάσουμε τα νέα properties του XMLHttpRequest: responseType και response, τα οποία πληροφορούν τον browser σχετικά με το format που θέλει ο client να του επιστραφούν τα δεδομένα.

xhr.responseType
Πριν αποστείλετε μια αίτηση (request), ορίστε το xhr.responseType σε "text", "arraybuffer", "blob", ή "document", ανάλογα με τις ανάγκες σας. Σημειωτέον, θέτοντας xhr.responseType = '' (ή μη αναφέροντάς το) προκαλούμε την default ανάθεση σε "text".
xhr.response
Μετά από μια επιτυχημένη αίτηση (request), το response property του xhr θα περιέχει τα δεδομένα που ζητήθηκαν ως DOMString, ArrayBuffer, Blob, ή Document (ανάλογα με την ανάθεση του responseType.)

Ας ανακτήσουμε λοπόν μια εικόνα ως Blob:

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

ArrayBuffer responses

To ArrayBuffer αποτελεί ένα γενικής χρήσης, ορισμένης χωρητικότητας (fixed-length) container για binary δεδομένα. Είναι πολύ χρήσιμο αν υπάρχει ανάγκη για έναν γενικό υποδοχέα (buffer) raw δεδομένων, αν και η πραγματική του δύναμη έγκειται στο γεγονός ότι επιτρέπει τη δημιουργία "όψεων (views)" των υποκείμενων δεδομένων χρησιμοποιώντας JavaScript typed arrays. Συγκεκριμένα, πολλαπλές όψεις μπορούν να κατασκευαστούν από μια πηγή ArrayBuffer. Για παράδειγμα, μπορείτε να χρησιμοποιήσετε μια 8-bit integer array που χρησιμοποιεί το ίδιο ArrayBuffer με μια υπάρχουσα 32-bit integer array από τα ίδια δεδομένα. Τα δεδομένα παραμένουν τα ίδια, απλώς δημιουργούνται διαφορετικές αναπαραστάσεις αυτών.

Για παράδειγμα, ο παρακάτω κώδικας ανακτά την ίδια εικόνα με πριν ως ArrayBuffer, αλλά αυτή τη φορά δημιουργεί μια unsigned 8-bit integer array από αυτό το data buffer:

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 responses

Αν επιθυμείτε να χειριστείτε απευθείας ένα Blob και/ή δε χρειάζεται να χειριστείτε κάποιο(α) από τα bytes του αρχείου, χρησιμοποιείστε 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 μπορεί να χρησιμοποιηθεί σε διάφορες περιπτώσεις, όπως είναι η αποθήκευσή του στο σύστημα αρχείων της HTML5, ή για τη δημιουργία μιας Blob URL, όπως είδαμε σε αυτό το παράδειγμα.

αποστολη δεδομενων

Η δυνατότητα μεταποθήκευσης (download) δεδομένων σε διαφορετικά formats είναι απαραίτητη, αλλά εξίσου απαραίτητη είναι και η αποστολή τους στον server. Το αρχικό XMLHttpRequest περιοριζόταν στην αποστολή DOMString ή Document (XML) δεδομένων. Πλέον, η μέθοδος send() δέχεται και τους ακόλουθους τύπους: DOMString, Document, FormData, Blob, File, ArrayBuffer. Τα παραδείγματα που ακολουθούν παρουσιάζουν την αποστολή δεδομένων για κάθε τύπο.

Αποστολη δεδομενων string: 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');

Ο παλιός τρόπος είναι παρόμοιος με τον νέο τρόπο χρήσης του XHR. Ο νέος τρόπος θέτει responseType='text'. Ακόμα και αν δεν υπήρχε αυτή η ανάθεση, θα είχαμε τα ίδια αποτελέσματα (χρήση default τιμής).

ΥΠΟΒΑΛΛΟΝΤΑς ΦΟΡΜΕΣ: xhr.send(FormData)

Πολλοί προγραμματιστές χρησιμοποιούν jQuery plugins ή άλλες βιβλιοθήκες για να χειριστούν AJAX υποβολές φορμών. Πλέον, είναι δυνατή η χρήση του FormData, ενός νέου τύπου δεδομένων συμβατό με το XHR2. Ο FormData είναι βολικός για δημιουργία HTML <form> on-the-fly, σε JavaScript. Αυτή η φόρμα μπορεί να αποσταλλεί μέσω 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);
}

Ουσιαστικά, δημιουργούμε δυναμικά <form> και <input>.

Προφανώς, δε χρειάζεται να δημιουργήσετε <form> εξ' αρχής. Αντικείμενα FormData objects αρκικοποιούνται από ένα υπάρχον HTMLFormElement της σελίδας. Για παράδειγμα:

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

Μια HTML form μπορεί να συμπεριλαμβάνει file uploads (π.χ. <input type="file">) τα οποία ο FormData μπορεί να χειριστεί. Απλά, προσθέστε το(α) αρχείο(α) και ο browser θα δημιουργήσει μια αίτηση multipart/form-data κατά την κλήση της 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);

Uploading αρχειου η blob: xhr.send(Blob)

Είναι επίσης εφικτή η αποστολή αρχείου ή Blob δεδομένων μέσω XHR. Υπόψη ότι όλα τα Αρχεία είναι Blob.

Το παράδειγμα αυτό δημιουργεί ένα νέο αρχείο κειμένου από την αρχή χρησιμοποιώντας τον Blob() constructor και μεταφέρει το αντίστοιχο Blob στον server. Επίσης, στο παράδειγμα καθορίζεται ένας handler που πληροφορεί τον χρήστη για την εξέλιξη της μεταφοράς:

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

Uploading πληθος απο bytes: xhr.send(ArrayBuffer)

Τέλος, μπορούμε να στείλουμε ArrayBuffers μέσω 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);
}

Cross Origin Resource Sharing (CORS)

H τεχνική CORS επιτρέπει στις web εφαρμογές ενός domain να απευθύνουν cross domain AJAX αιτήσεις σε άλλα domain. Το μόνο που απαιτείται από τον server είναι η αποστολή ενός response header.

Επιτρεποντας CORS requests

Έστω ότι η web εφαρμογή βρίσκεται στο example.com κια έστω ότι θέλουμε να τραβήξουμε δεδομένα από το www.example2.com. Στο παραδοσιακό AJAX, η αίτηση θα α αποτύγχανε και ο browser θα έστελνε ένα origin mismatch error. Με την CORS, ο www.example2.com μπορεί να διαλέξει αν θα επιτρέπει αιτήσεις από τον example.com απλά προσθέτοντας το header:

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

Ο header Access-Control-Allow-Origin μπορεί να προστεθεί σε μια μόνο ιστοσελίδα ή σε ένα δικτυακό τόπο η σε ολόκληρο domain. Για να επιτρέψετε σε οποιοδήποτε domain να κάνει αιτήσεις σε εσάς θέστε:

Access-Control-Allow-Origin: *

Η πολιτική CORS ενός server φαίνεται μέσα από τα Developer Tools (Google Chrome) και πιο συγκεκριμένα μέσα από τα headers μιας απάντησης: Access-Control-Allow-Origin:

Access-Control-Allow-Origin header on html5rocks.com
Access-Control-Allow-Origin header on html5rocks.com

Ενδείκνυται η εφαρμογή ελεύθερης χρήσης cross-origin requests enable CORS αν τα δεδομένα ενός server είναι δημόσια!

Δημιουργωντας μια αιτηση cross-domain

Αν ένας server επιτρέπει CORS, η δημιουργία μιας αίτησης cross-origin δε διαφέρει από μια απλή XMLHttpRequest αίτηση. Για παράδειγμα, εδώ έχουμε μια αίτηση από τον example.com στον 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();

πρακτικα παραδειγματα

Download + αποθηκευση αρχειων στο HTML5 file system

Έστω ότι έχετε στον server μια image gallery και θέλετε να φέρετε κάποιες εικόνες στον client τις οποίες και να αποθηκεύσετε στο HTML5 File System. Ένας τρόπος να το επιτύχετε είναι να αιτηθείτε τις εικόνες ως Blobs και να τις αποθηκεύσετε χρησιμοποιώντας το 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();

Σημείωση: για τη χρήση αυτού του κώδικα, δείτε: browser support & storage limitations in the "Ανακαλύπτοντας το FileSystem API.

Τεμαχιζοντας ενα αρχειο και μεταφεροντας καθε τμημα του

Χρησιμοποιώντας το File API, μπορούμε να ελαχιστοποιήσουμε τον κόπο μεταφοράς ενός μεγάλου αρχείου. Η τεχνική υπαγορεύει το τεμαχισμό της μεταφοράς σε μικρότερα τμήματα, τη δημιουργία ενός XHR για κάθε τμήμα, και η επανακόλληση του αρχείου στον server. Η τεχνική αυτή είναι παρόμοια με αυτή που χρησιμοποιεί το GMail για να μεταφέρει επισυνάψεις τόσο γρήγορα.

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

})();

Αναφορες