XMLHttpRequest2'deki yeni püf noktaları

Giriş

HTML5 evrenindeki adsız kahramanlardan biri XMLHttpRequest. Daha net ifade etmek gerekirse XHR2, HTML5 değildir. Ancak bu, tarayıcı tedarikçilerinin çekirdek platforma yaptıkları artımlı iyileştirmelerin bir parçasıdır. XHR2'yi, günümüzün karmaşık web uygulamalarının ayrılmaz bir parçası olduğu için yeni hediye paketimize ekliyorum.

Eski arkadaşımız kocaman bir şekilde yenilendi ancak birçok kişi yeni özelliklerin farkında değil. XMLHttpRequest Düzey 2, çapraz kaynak istekleri, ilerleme olaylarının yüklenmesi ve ikili program verilerinin yüklenmesi/indirilmesi gibi web uygulamalarımızdaki karmaşık saldırılara son veren bir dizi yeni özellik sunuyor. Bu API'ler, AJAX'ın File System API, Web Audio API ve WebGL gibi yeni teknoloji HTML5 API'lerinin çoğuyla uyumlu bir şekilde çalışmasına olanak tanır.

Bu eğitimde, özellikle dosyalarla çalışmak için kullanılabilecek özellikler olmak üzere XMLHttpRequest ürünündeki yeni özelliklerden bazıları ele alınmaktadır.

Veriler alınıyor

Dosyaları ikili blob olarak getirmek XHR ile zor bir işti. Teknik olarak bu imkansızdı. İyi belgelenmiş bir yöntem, aşağıda gösterildiği gibi MIME türünün kullanıcı tanımlı bir karakter kümesiyle geçersiz kılınmasıdır.

Resim getirmenin eski yolu:

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

Bu işe yarar olsa da responseText içinde geri aldığınız şey bir ikili blob değildir. Resim dosyasını temsil eden bir ikili dizedir. Verileri işlenmemiş olarak geri iletmesi için sunucuyu kandırıyoruz. Bu küçük cevher işe yarıyor olsa da, buna kara büyü diyeceğim ve buna karşı öneride bulunacağım. Verileri istediğiniz bir biçime zorlamak için karakter kodu korsanlığına ve dize manipülasyonuna her başvurduğunuzda bu bir sorundur.

Yanıt biçimi belirtme

Önceki örnekte, sunucunun MIME türünü geçersiz kılarak ve yanıt metnini ikili dize olarak işleyerek görüntüyü ikili program "dosya" olarak indirdik. Bunun yerine, tarayıcıya verilerin hangi biçimde döndürülmesini istediğimizi bildirmek için XMLHttpRequest'in yeni responseType ve response özelliklerinden yararlanalım.

xhr.responseType
İstek göndermeden önce, veri ihtiyaçlarınıza göre xhr.responseType özelliğini "text", "arraybuffer", "blob" veya "document" olarak ayarlayın. xhr.responseType = '' ayarlandığında (veya atlandığında) yanıtın varsayılan olarak "text" (metin) şeklinde ayarlanacağını unutmayın.
xhr.response
Başarılı bir istekten sonra, xhr'nin yanıt özelliği istenen verileri (responseType için ayarlanana bağlı olarak) DOMString, ArrayBuffer, Blob veya Document olarak içerir.

Bu yeni mükemmellikle önceki örneği üzerinde çalışabiliriz, ancak bu sefer resmi dize yerine bir Blob olarak getiriyoruz:

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

Çok daha iyi!

ArrayBuffer yanıtları

ArrayBuffer, ikili veriler için sabit uzunlukta genel bir container'dır. Genelleştirilmiş bir ham veri tamponuna ihtiyacınız varsa bu yöntem çok kullanışlıdır, ancak bu cihazların asıl gücü, JavaScript türünde diziler kullanarak temel verilerin "görünümlerini" oluşturabilmenizdir. Aslında tek bir ArrayBuffer kaynağından birden çok görünüm oluşturulabilir. Örneğin, aynı verideki mevcut bir 32 bitlik tam sayı dizisiyle aynı ArrayBuffer değerini paylaşan 8 bitlik bir tam sayı dizisi oluşturabilirsiniz. Temel veriler aynı kalır, yalnızca bunların farklı temsillerini oluştururuz.

Örnek olarak, aşağıda ArrayBuffer ile aynı görüntüyü getiriyoruz ancak bu kez, bu veri arabelleğinden imzalanmamış bir 8 bitlik tam sayı dizisi oluşturuyor:

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 yanıtları

Doğrudan bir Blob ile çalışmak istiyorsanız ve/veya dosyanın baytlarından herhangi birini değiştirmeniz gerekmiyorsa xhr.responseType='blob' kullanın:

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; indexedDB'ye kaydetmek, HTML5 Dosya Sistemi'ne yazmak veya bu örnekte gösterildiği gibi Blob URL'si oluşturmak dahil olmak üzere çeşitli yerlerde kullanılabilir.

Veri gönderiliyor

Verileri farklı biçimlerde indirebilmek harika bir şey, ancak bu zengin biçimleri ana merkeze (sunucu) geri gönderemezsek bizi hiçbir yere götürmez. XMLHttpRequest, bir süre boyunca DOMString veya Document (XML) verilerini göndermekle sınırlandırdı. Ama artık değil. Yenilenen send() yöntemi, şu türlerden herhangi birini kabul edecek şekilde geçersiz kılındı: DOMString, Document, FormData, Blob, File, ArrayBuffer. Bu bölümün geri kalanındaki örneklerde, her bir türü kullanarak veri gönderme işlemi gösterilmektedir.

Dize verileri gönderiliyor: 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');

Doğru snippet biraz farklı olsa da burada yeni bir şey yoktur. Karşılaştırma için responseType='text' değerini ayarlar. Bu çizginin atlanması da aynı sonuçları verir.

Formları gönderme: xhr.send(FormData)

Muhtemelen çoğu kişi AJAX form gönderimlerini işlemek için jQuery eklentileri veya diğer kitaplıkları kullanmaya alışıktır. Bunun yerine, XHR2 için tasarlanan başka bir yeni veri türü olan FormData'i kullanabiliriz. FormData, JavaScript ile çalışırken HTML <form> oluşturmak için kullanışlıdır. Bu form daha sonra AJAX kullanılarak gönderilebilir:

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

Esasen, dinamik olarak bir <form> oluşturuyor ve ekleme yöntemini çağırarak buna <input> değerleri ekliyoruz.

Elbette, sıfırdan bir <form> oluşturmanız gerekmez. FormData nesne, sayfada bulunan HTMLFormElement öğesinden başlatılabilir. Örneğin:

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

Bir HTML formu dosya yüklemelerini (ör. <input type="file">) içerebilir ve FormData bu işi de halledebilir. Dosyaları eklemeniz yeterlidir. send() çağrıldığında tarayıcı bir multipart/form-data isteği oluşturur:

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

Dosya veya blob yükleme: xhr.send(Blob)

Ayrıca File veya Blob verilerini XHR kullanarak gönderebiliriz. Tüm File öğelerinin Blob olduğunu, bu nedenle her ikisinin de burada çalıştığını unutmayın.

Bu örnek, Blob() oluşturucuyu kullanarak sıfırdan yeni bir metin dosyası oluşturur ve bu Blob dosyasını sunucuya yükler. Kod, kullanıcıyı yüklemenin ilerlemesi konusunda bilgilendirmek için bir işleyici de ayarlar:

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

Bir bayt yığını yükleniyor: xhr.send(ArrayBuffer)

Son olarak, ArrayBuffer'leri XHR yükü olarak gönderebiliriz.

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

Merkezler Arası Kaynak Paylaşımı (CORS)

CORS bir alandaki web uygulamalarının başka bir alana alanlar arası AJAX istekleri yapmasına olanak tanır. Sunucu tarafından yalnızca tek bir yanıt üstbilgisinin gönderilmesi gerektiğinden, bu özelliği etkinleştirmek son derece kolaydır.

CORS isteklerini etkinleştirme

Uygulamanızın example.com üzerinde çalıştığını ve www.example2.com kaynağından veri çekmek istediğinizi varsayalım. Normalde bu tür bir AJAX çağrısı yapmaya çalışırsanız istek başarısız olur ve tarayıcı bir kaynak uyuşmazlığı hatası verir. CORS sayesinde www.example2.com, yalnızca bir başlık ekleyerek example.com ürününden gelen isteklere izin vermeyi seçebilir:

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

Access-Control-Allow-Origin, bir sitenin altındaki tek bir kaynağa veya alan genelinde eklenebilir. Herhangi bir alan adının size istek göndermesine izin vermek için şu ayarları yapın:

Access-Control-Allow-Origin: *

Aslında, bu site (html5rocks.com), tüm sayfalarında CORS'yi etkinleştirmiştir. Geliştirici Araçları'nı başlattığınızda yanıtımızda Access-Control-Allow-Origin ifadesini göreceksiniz:

html5rocks.com adresindeki Access-Control-Allow-Origin üstbilgisi
html5rocks.com adresindeki "Access-Control-Allow-Origin" üstbilgisi

Kaynaklar arası istekleri etkinleştirmek kolaydır. Bu nedenle, lütfen verileriniz herkese açıksa lütfen CORS'yi etkinleştirin.

Alanlar arası istek yapma

Sunucu uç noktası CORS'yi etkinleştirdiyse kaynaklar arası istek, normal bir XMLHttpRequest isteğinden farklı değildir. Örneğin, example.com adlı kullanıcının artık www.example2.com adlı alıcıya iletebileceği bir istek şu şekildedir:

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

Pratik örnekler

Dosyaları indirme ve HTML5 dosya sistemine kaydetme

Bir resim galerinizin olduğunu ve bir grup resmi getirip HTML5 Dosya Sistemi'ni kullanarak yerel olarak kaydetmek istediğinizi varsayalım. Bunu başarmanın yollarından biri, resimleri Blob biçiminde istemek ve resimleri FileWriter kullanarak yazmaktır:

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

Bir dosyayı dilimleme ve her bir kısmını yükleme

Dosya API'lerini kullanarak büyük bir dosyayı yükleme işini en aza indirebiliriz. Teknik, yüklemeyi birden fazla parçaya bölmek, her kısım için bir XHR oluşturmak ve dosyayı sunucuda bir araya getirmektir. Bu, Gmail'in büyük ekleri hızlı bir şekilde yüklemesine benzer. Bu tür bir teknik, Google App Engine'in 32 MB'lık http istek sınırını aşmak için de kullanılabilir.

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

})();

Burada, dosyayı sunucuda yeniden oluşturmak için gereken kod gösterilmez.

Referanslar