Trik baru di XMLHttpRequest2

Pengantar

Salah satu pahlawan tanpa tanda jasa di dunia HTML5 adalah XMLHttpRequest. Secara ketat, XHR2 bukan HTML5. Namun, ini adalah bagian dari peningkatan inkremental yang dilakukan vendor browser pada platform inti. Saya menyertakan XHR2 dalam kumpulan fitur baru kami karena XHR2 memainkan peran yang sangat penting dalam aplikasi web yang kompleks saat ini.

Ternyata, teman lama kita ini telah mengalami perubahan besar, tetapi banyak orang tidak mengetahui fitur-fiturnya yang baru. XMLHttpRequest Level 2 memperkenalkan serangkaian kemampuan baru yang mengakhiri hack rumit di aplikasi web kita; hal-hal seperti permintaan lintas origin, peristiwa progres upload, dan dukungan untuk mengupload/mendownload data biner. Hal ini memungkinkan AJAX berfungsi bersama dengan banyak API HTML5 terbaru seperti File System API, Web Audio API, dan WebGL.

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

Mengambil data

Mengambil file sebagai blob biner sangat sulit dengan XHR. Secara teknis, hal itu bahkan tidak mungkin. Salah satu trik yang telah didokumentasikan dengan baik melibatkan penggantian jenis mime dengan set karakter yang ditentukan pengguna seperti yang terlihat di bawah.

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 berhasil, yang sebenarnya Anda dapatkan di responseText bukan blob biner. Ini adalah string biner yang mewakili file gambar. Kita mengelabui server agar meneruskan data kembali, tanpa diproses. Meskipun permata kecil ini berfungsi, saya akan menyebutnya ilmu hitam dan menyarankan agar tidak menggunakannya. Setiap kali Anda menggunakan hack kode karakter dan manipulasi string untuk memaksa data ke dalam format yang diinginkan, itu adalah 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 format yang kita inginkan untuk ditampilkan data.

xhr.responseType
Sebelum mengirim permintaan, tetapkan xhr.responseType ke "text", "arraybuffer", "blob", atau "document", bergantung pada kebutuhan data Anda. Perhatikan, menyetel xhr.responseType = '' (atau menghapusnya) 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 apa yang ditetapkan untuk responseType.)

Dengan fitur baru yang luar biasa 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 tetap generik untuk data biner. Array ini sangat berguna jika Anda memerlukan buffering data mentah umum, tetapi kekuatan sebenarnya dari array ini adalah Anda dapat membuat "tampilan" data pokok menggunakan array berjenis 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 pokoknya tetap sama, kita hanya membuat representasi yang berbeda.

Sebagai contoh, kode berikut mengambil gambar yang sama sebagai ArrayBuffer, tetapi kali ini, membuat array bilangan bulat 8-bit tanpa tanda tangan 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 apa pun, 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 File System HTML5, atau membuat URL Blob, seperti yang terlihat dalam contoh ini.

Mengirim data

Kemampuan untuk mendownload data dalam berbagai format memang bagus, tetapi tidak akan membawa kita ke mana pun jika kita tidak dapat mengirim format lengkap ini kembali ke basis utama (server). XMLHttpRequest telah membatasi kami untuk mengirim data DOMString atau Document (XML) selama beberapa waktu. Jangan khawatir. Metode send() yang diubah telah diganti untuk menerima salah satu jenis berikut: DOMString, Document, FormData, Blob, File, ArrayBuffer. Contoh di bagian lain bagian ini menunjukkan pengiriman data menggunakan setiap 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 sebelah kanan sedikit berbeda. Fungsi ini menetapkan responseType='text' untuk perbandingan. Sekali lagi, menghapus baris tersebut akan menghasilkan hasil yang sama.

Mengirim formulir: xhr.send(FormData)

Banyak orang mungkin 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 digunakan untuk membuat <form> HTML secara langsung, di JavaScript. Formulir tersebut kemudian dapat dikirimkan 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 menambahkan nilai <input> ke dalamnya dengan memanggil metode tambahan.

Tentu saja, Anda tidak perlu membuat <form> dari awal. Objek FormData dapat diinisialisasi dari dan HTMLFormElement yang 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, sehingga 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 sekumpulan 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 mengaktifkannya, hanya memerlukan satu header respons yang dikirim oleh server.

Mengaktifkan permintaan CORS

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

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

Access-Control-Allow-Origin dapat ditambahkan ke satu resource di bawah situs atau di seluruh domain. Untuk mengizinkan domain mana 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 html5rocks.com
Header`Access-Control-Allow-Origin` di html5rocks.com

Mengaktifkan permintaan lintas-asal itu mudah, jadi harap aktifkan CORS jika data Anda bersifat publik.

Membuat permintaan lintas-domain

Jika endpoint server telah mengaktifkan CORS, membuat permintaan lintas origin tidak berbeda dengan permintaan XMLHttpRequest normal. Misalnya, berikut adalah permintaan yang kini dapat dibuat oleh 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

Misalnya, Anda memiliki galeri gambar dan ingin mengambil banyak gambar, lalu menyimpannya secara lokal menggunakan Sistem File HTML5. Salah satu cara untuk melakukannya adalah dengan meminta gambar sebagai Blob dan menulisnya 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 mengupload setiap bagian

Dengan menggunakan File API, kita dapat meminimalkan pekerjaan untuk mengupload file berukuran besar. Tekniknya adalah memotong upload menjadi beberapa bagian, membuat XHR untuk setiap bagian, dan menggabungkan file di server. Hal ini mirip dengan cara Gmail mengupload lampiran besar dengan sangat cepat. Teknik tersebut juga dapat digunakan untuk mengakali batas permintaan HTTP 32 MB Google App Engine.

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