Trik baru di XMLHttpRequest2

Pengantar

Salah satu pahlawan tanpa tanda jasa di semesta HTML5 adalah XMLHttpRequest. Sebenarnya XHR2 bukan HTML5. Namun, ini merupakan bagian dari peningkatan inkremental yang dilakukan vendor browser pada platform inti. Saya memasukkan XHR2 ke dalam tas barang baru kami karena alat ini memainkan bagian integral dalam aplikasi web yang kompleks saat ini.

Ternyata teman lama kami punya perubahan besar tetapi banyak orang tidak menyadari fitur barunya. XMLHttpRequest Level 2 memperkenalkan banyak kemampuan baru yang mengakhiri peretasan rumit di aplikasi web kami; fitur-fitur seperti permintaan lintas origin, mengupload peristiwa progres, dan dukungan untuk mengupload/mendownload data biner. Dengan API ini, AJAX dapat berfungsi bersama dengan banyak API HTML5 yang paling canggih seperti File System API, Web Audio API, dan WebGL.

Tutorial ini menyoroti beberapa fitur baru di XMLHttpRequest, terutama yang dapat digunakan untuk bekerja dengan file.

Mengambil data

Pengambilan file sebagai blob biner sangat menyulitkan dengan XHR. Secara teknis, hal itu tidak memungkinkan. Salah satu trik yang telah didokumentasikan dengan baik melibatkan penggantian jenis mime dengan charset yang ditentukan pengguna seperti yang terlihat di bawah ini.

Cara lama untuk mengambil gambar:

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

Meskipun cara ini berfungsi, yang sebenarnya Anda dapatkan di responseText bukanlah blob biner. Ini adalah string biner yang mewakili file gambar. Kami menipu server agar meneruskan data kembali, yang belum diproses. Meskipun permata kecil ini berhasil, saya akan menyebutnya ilmu hitam dan menyarankannya. Setiap kali Anda melakukan peretasan kode karakter dan manipulasi string untuk memaksa data ke dalam format yang diinginkan, itulah masalah.

Menentukan format respons

Pada contoh sebelumnya, kita mendownload gambar sebagai "file" biner dengan mengganti jenis mime server dan memproses teks respons sebagai string biner. Sebagai gantinya, mari kita manfaatkan properti responseType dan response baru XMLHttpRequest untuk memberi tahu browser tentang format yang kita inginkan untuk menampilkan data.

xhr.responseType
Sebelum mengirim permintaan, tetapkan xhr.responseType ke "text", "arraybuffer", "blob", atau "document", bergantung pada kebutuhan data Anda. Perhatikan, menetapkan xhr.responseType = '' (atau menghapus) akan menetapkan respons default ke "text".
xhr.response
Setelah permintaan berhasil, properti respons xhr akan berisi data yang diminta sebagai DOMString, ArrayBuffer, Blob, atau Document (bergantung pada yang ditetapkan untuk responseType.)

Dengan keunikan baru ini, kita dapat mengerjakan ulang contoh sebelumnya, tetapi kali ini, ambil gambar sebagai Blob, bukan string:

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

Jauh lebih bagus!

Respons ArrayBuffer

ArrayBuffer adalah penampung panjang yang tetap dan generik untuk data biner. Fungsi ini sangat berguna jika Anda memerlukan buffer umum data mentah. Namun, keunggulan sebenarnya di balik cara ini adalah Anda dapat membuat "tampilan" data pokok menggunakan array yang diketik JavaScript. Bahkan, beberapa tampilan dapat dibuat dari satu sumber ArrayBuffer. Misalnya, Anda dapat membuat array bilangan bulat 8-bit yang memiliki ArrayBuffer yang sama dengan array bilangan bulat 32-bit yang ada dari data yang sama. Data yang mendasarinya tetap sama, kita hanya membuat representasinya yang berbeda.

Sebagai contoh, kode berikut mengambil gambar yang sama seperti ArrayBuffer, tetapi kali ini, membuat array bilangan bulat 8-bit tanpa label dari buffer data tersebut:

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

Respons blob

Jika Anda ingin bekerja langsung dengan Blob dan/atau tidak perlu memanipulasi byte file, gunakan 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 dapat digunakan di sejumlah tempat, termasuk menyimpannya ke indexedDB, menulisnya ke Sistem File HTML5, atau membuat URL Blob, seperti yang terlihat dalam contoh ini.

Mengirim data

Dapat mendownload data dalam berbagai format memang bagus, tetapi hal ini tidak akan membawa kita ke mana pun jika kita tidak dapat mengirim format yang kaya ini kembali ke home base (server). XMLHttpRequest telah membatasi kita untuk mengirim data DOMString atau Document (XML) selama beberapa waktu. Jangan lagi. Metode send() yang diubah telah diganti untuk menerima salah satu jenis berikut: DOMString, Document, FormData, Blob, File, ArrayBuffer. Contoh di bagian selanjutnya menunjukkan pengiriman data menggunakan masing-masing jenis.

Mengirim data 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');

Tidak ada yang baru di sini, meskipun cuplikan yang tepat sedikit berbeda. Class ini menetapkan responseType='text' untuk perbandingan. Sekali lagi, menghilangkan garis tersebut memberikan hasil yang sama.

Mengirimkan formulir: xhr.send(FormData)

Banyak orang mungkin sudah terbiasa menggunakan plugin jQuery atau library lain untuk menangani pengiriman formulir AJAX. Sebagai gantinya, kita dapat menggunakan FormData, jenis data baru lainnya yang dibuat untuk XHR2. FormData mudah untuk membuat <form> HTML dengan cepat, di JavaScript. Formulir tersebut kemudian dapat dikirim menggunakan 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);
}

Pada dasarnya, kita hanya membuat <form> secara dinamis dan melekat pada nilai <input> ke dalamnya dengan memanggil metode penambahan.

Tentu saja, Anda tidak perlu membuat <form> dari awal. Objek FormData dapat diinisialisasi dari dan HTMLFormElement yang sudah ada di halaman. Contoh:

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

Formulir HTML dapat menyertakan upload file (misalnya, <input type="file">) dan FormData juga dapat menanganinya. Cukup tambahkan file dan browser akan membuat permintaan multipart/form-data saat send() dipanggil:

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

Mengupload file atau blob: xhr.send(Blob)

Kita juga dapat mengirim data File atau Blob menggunakan XHR. Perlu diingat bahwa semua File adalah Blob, jadi keduanya berfungsi di sini.

Contoh ini membuat file teks baru dari awal menggunakan konstruktor Blob() dan mengupload Blob tersebut ke server. Kode ini juga menyiapkan pengendali untuk memberi tahu pengguna tentang progres upload:

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

Mengupload potongan byte: xhr.send(ArrayBuffer)

Terakhir, kita dapat mengirim ArrayBuffer sebagai payload 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)

CORS memungkinkan aplikasi web di satu domain membuat permintaan AJAX lintas domain ke domain lain. Sangat mudah untuk diaktifkan, hanya memerlukan satu header respons untuk dikirim oleh server.

Mengaktifkan permintaan CORS

Misalnya aplikasi Anda berada di example.com dan Anda ingin menarik data dari www.example2.com. Biasanya, jika Anda mencoba melakukan panggilan AJAX jenis ini, permintaan akan gagal dan browser akan menampilkan error ketidakcocokan origin. Dengan CORS, www.example2.com dapat memilih untuk mengizinkan permintaan dari example.com hanya dengan menambahkan header:

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

Access-Control-Allow-Origin dapat ditambahkan ke resource tunggal di situs atau di seluruh domain. Untuk mengizinkan domain apa pun membuat permintaan kepada Anda, tetapkan:

Access-Control-Allow-Origin: *

Bahkan, situs ini (html5rocks.com) telah mengaktifkan CORS di semua halamannya. Aktifkan Developer Tools dan Anda akan melihat Access-Control-Allow-Origin dalam respons kami:

Header Access-Control-Allow-Origin di html5 Grups.com
Header`Access-Control-Allow-Origin` di html5Rock.com

Mengaktifkan permintaan lintas origin itu mudah. Jadi, harap aktifkan CORS jika data Anda bersifat publik.

Membuat permintaan lintas-domain

Jika endpoint server telah mengaktifkan CORS, pembuatan permintaan lintas asal tidak berbeda dengan permintaan XMLHttpRequest biasa. Misalnya, berikut adalah permintaan yang sekarang dapat dibuat example.com ke 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();

Contoh praktis

Mendownload + menyimpan file ke sistem file HTML5

Misalkan Anda memiliki galeri gambar dan ingin mengambil sekumpulan gambar, lalu menyimpannya secara lokal menggunakan Sistem File HTML5. Salah satu cara untuk melakukannya adalah dengan meminta gambar sebagai Blob dan menuliskannya menggunakan 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();

Memotong file dan mengunggah setiap bagiannya

Dengan menggunakan File API, kita dapat meminimalkan pekerjaan untuk mengupload file besar. Tekniknya adalah membagi upload menjadi beberapa bagian, membuat XHR untuk setiap bagian, dan menyatukan file di server. Cara ini mirip dengan cara Gmail mengupload lampiran besar dengan cepat. Teknik tersebut juga dapat digunakan untuk mengatasi batas permintaan http Google App Engine sebesar 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);

})();

Yang tidak ditampilkan di sini adalah kode untuk merekonstruksi file di server.

Referensi