XMLHttpRequest2'deki yeni püf noktaları

Giriş

HTML5 dünyasının gizli kahramanlarından biri XMLHttpRequest'tir. XHR2, teknik olarak HTML5 değildir. Ancak bu, tarayıcı tedarikçi firmalarının temel platformda yaptığı artımlı iyileştirmelerin bir parçasıdır. XHR2'yi, günümüzün karmaşık web uygulamalarında önemli bir rol oynadığı için yeni avantajlar paketimize dahil ediyoruz.

Eski dostumuz büyük bir makyaj geçirdi ancak birçok kullanıcı yeni özelliklerinden haberdar değil. XMLHttpRequest 2. Seviye, web uygulamalarımızdaki karmaşık saldırılara son veren bir dizi yeni özellik sunar. Kaynaklar arası istekler, yükleme ilerleme etkinlikleri ve ikili veri yükleme/indirme desteği gibi özelliklerden yararlanabilirsiniz. Bu özellikler, AJAX'in File System API, Web Audio API ve WebGL gibi en yeni HTML5 API'lerinin çoğuyla birlikte çalışmasına olanak tanır.

Bu eğitimde, XMLHttpRequest'teki yeni özelliklerden bazılarına (özellikle dosyalarla çalışmak için kullanılabileceklere) yer verilmiştir.

Veriler alınıyor

XHR ile bir dosyayı ikili blob olarak almak zordu. Teknik olarak mümkün bile değildi. İyi belgelenmiş bir hile, mime türünü aşağıda görüldüğü gibi kullanıcı tanımlı bir karakter kümesiyle geçersiz kılmaktı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 yöntem işe yarar ancak responseText döndürülen değer aslında ikili bir blob değildir. Resim dosyasını temsil eden ikili bir dizedir. Sunucuyu kandırarak verileri işlenmemiş şekilde geri iletmesini sağlıyoruz. Bu küçük mücevher işe yaramasına rağmen, kara büyü olarak adlandıracağım ve bu yöntemi kullanmamanızı önereceğim. Verileri istenilen biçime zorlamak için karakter kodu saldırılarına ve dize değiştirmeye 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 resmi ikili "dosya" olarak indirdik. Bunun yerine, XMLHttpRequest'ın yeni responseType ve response özelliklerinden yararlanarak tarayıcıya verilerin hangi biçimde döndürülmesini istediğimizi bildirelim.

xhr.responseType
İstek göndermeden önce xhr.responseType değerini veri ihtiyaçlarınıza bağlı olarak "text", "arraybuffer", "blob" veya "document" olarak ayarlayın. xhr.responseType = '' ayarının (veya atlamanın) yanıtı varsayılan olarak "text" olarak ayarlayacağını unutmayın.
xhr.response
Başarılı bir istek sonrasında, xhr'nin response mülkü istenen verileri DOMString, ArrayBuffer, Blob veya Document olarak içerir (responseType için ayarlanan değere bağlı olarak).

Bu yeni ve muhteşem özellik sayesinde önceki örneği yeniden işleyebiliriz. Ancak bu kez resmi dize yerine Blob olarak getiririz:

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 genel sabit uzunlukta bir kapsayıcıdır. Genelleştirilmiş bir ham veri arabelleğine ihtiyacınız varsa bu veri türleri çok kullanışlıdır. Ancak bu türlerin asıl gücü, JavaScript türüne sahip dizileri kullanarak temel verilerin "görüntülerini" oluşturabilmenizdir. Aslında tek bir ArrayBuffer kaynağından birden fazla görünüm oluşturulabilir. Örneğin, aynı verilerden gelen 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 farklı gösterimleri oluştururuz.

Örneğin, aşağıdaki kod aynı resmimizi ArrayBuffer olarak getirir ancak bu kez bu veri arabelleğinden imzasız 8 bitlik bir tam sayı dizisi oluşturur:

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 kaydetme, HTML5 Dosya Sistemi'ne yazma veya bu örnekte görüldüğü gibi Blob URL'si oluşturma dahil olmak üzere çeşitli yerlerde kullanılabilir.

Veri gönderme

Verileri farklı biçimlerde indirebilmek harika bir şey olsa da bu zengin biçimleri ana merkeze (sunucuya) geri gönderemezsek hiçbir yere varamayız. XMLHttpRequest, bir süredir DOMString veya Document (XML) verileri göndermemizi sınırlandırdı. Artık değil. Yenilenmiş bir send() yöntemi, aşağıdaki 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 tür kullanılarak veri gönderme gösterilmektedir.

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

Burada yeni bir şey yok ancak sağdaki snippet biraz farklı. Karşılaştırma için responseType='text' değerini ayarlar. Bu satırın çıkarılması da aynı sonuçları verir.

Form gönderme: xhr.send(FormData)

Birçok kullanıcı, AJAX form gönderimlerini işlemek için jQuery eklentilerini veya diğer kitaplıkları kullanmaya alışmış olabilir. Bunun yerine, XHR2 için tasarlanmış yeni bir veri türü olan FormData'i kullanabiliriz. FormData JavaScript'de anında 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şturup append yöntemini çağırarak <input> değerlerini ekliyoruz.

Elbette sıfırdan <form> oluşturmanız gerekmez. FormData nesneleri, sayfadaki mevcut HTMLFormElement öğelerinden 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.
}

HTML formları dosya yüklemeleri (ör. <input type="file">) içerebilir ve FormData bu tür yüklemeleri de işleyebilir. Dosyaları eklemeniz yeterlidir. Tarayıcı, send() çağrıldığında 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)

XHR kullanarak File veya Blob verilerini de gönderebiliriz. Tüm File değerlerinin Blob değerleri olduğunu unutmayın. Bu nedenle, burada her iki değer de kullanılabilir.

Bu örnekte, Blob() kurucusunu kullanarak sıfırdan yeni bir metin dosyası oluşturulur ve bu Blob sunucuya yüklenir. Kod, kullanıcıyı yüklemenin ilerleme durumu hakkında bilgilendirmek için bir işleyici de oluşturur:

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

Bayt kümesi yükleme: xhr.send(ArrayBuffer)

Son olarak, XHR'nin yük verisi olarak ArrayBuffer 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 isteği göndermesine olanak tanır. Etkinleştirmek son derece basittir ve sunucu tarafından tek bir yanıt başlığının gönderilmesi yeterlidir.

CORS isteklerini etkinleştirme

Uygulamanızın example.com'te olduğunu ve www.example2.com'ten veri almak 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 ile www.example2.com, yalnızca bir başlık ekleyerek example.com'ten 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ın tamamına eklenebilir. Herhangi bir alanın size istek göndermesine izin vermek için şunları ayarlayın:

Access-Control-Allow-Origin: *

Bu site (html5rocks.com) aslında tüm sayfalarında CORS'u etkinleştirmiştir. Geliştirici Araçları'nı açın. Yanıtımızda Access-Control-Allow-Origin simgesini görürsünüz:

html5rocks.com&#39;daki Access-Control-Allow-Origin üstbilgisi
html5rocks.com'daki "Access-Control-Allow-Origin" üstbilgisi

Kaynaklar arası isteklerin etkinleştirilmesi kolaydır. Bu nedenle, verileriniz herkese açıksa lütfen CORS'u etkinleştirin.

Web alanları arası istek yapma

Sunucu uç noktası CORS'u etkinleştirdiyse kaynak arası istek göndermek normal bir XMLHttpRequest isteğinde bulunmaktan farklı değildir. Örneğin, example.com'in www.example2.com'a gönderebileceği bir istek aşağıda verilmiştir:

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ı indirip HTML5 dosya sistemine kaydetme

Bir resim galeriniz olduğunu ve bir grup resim almak istediğinizi, ardından bunları HTML5 Dosya Sistemi'ni kullanarak yerel olarak kaydetmek istediğinizi varsayalım. Bunu yapmanın bir yolu, resimleri Blob olarak istemek ve FileWriter kullanarak yazmak olabilir:

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

Dosyaları dilimlere ayırma ve her bir dilimi yükleme

Dosya API'lerini kullanarak büyük bir dosyayı yüklemeyle ilgili işlemleri en aza indirebiliriz. Teknik, yüklemeyi birden fazla parçaya bölmek, her bölüm için bir XHR oluşturmak ve dosyayı sunucuda birleştirmektir. Bu, Gmail'in büyük ekleri çok hızlı bir şekilde yüklemesine benzer. Bu tür bir teknik, Google App Engine'ın 32 MB 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, dosyanın sunucuda yeniden oluşturulmasına yönelik kod gösterilmez.

Referanslar