Getirme API'si ile akış istekleri

Jake Archibald
Jake Archibald

Chromium 105'ten itibaren Streams API'yi kullanarak mesajın tamamını almadan önce istek başlatabilirsiniz.

Bu özelliği şu amaçlarla kullanabilirsiniz:

  • Sunucuyu ısıtın. Diğer bir deyişle, kullanıcı bir metin giriş alanına odaklandıktan sonra isteği başlatabilir ve tüm üstbilgileri kaldırabilir, ardından kullanıcının girdiği verileri göndermeden önce "gönder" düğmesine basmasını bekleyebilirsiniz.
  • Müşteride oluşturulan ses, video veya giriş verileri gibi verileri kademeli olarak gönderin.
  • HTTP/2 veya HTTP/3 üzerinden web soketlerini yeniden oluşturun.

Ancak bu düşük düzey bir web platformu özelliği olduğundan benim fikirlerimle sınırlı olmayın. İstekte yayın için çok daha heyecan verici bir kullanım alanı düşünebilirsiniz.

Demo

Bu resimde, kullanıcıdan sunucuya veri aktarımı ve gerçek zamanlı olarak işlenebilecek verileri geri gönderme işlemleri gösterilmektedir.

Evet, bu çok yaratıcı bir örnek değil. Basit tutmak istedim.

Peki, bu nasıl çalışır?

Daha önce, get akışlarının heyecan verici maceralarında

Yanıt akışları bir süredir tüm modern tarayıcılarda kullanılabiliyor. Bu işlevler, yanıtın sunucudan geldikçe bölümlerine erişmenize olanak tanır:

const response = await fetch(url);
const reader = response.body.getReader();

while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  console.log('Received', value);
}

console.log('Response fully received');

Her value, Uint8Array bayttır. Aldığınız dizilerin sayısı ve boyutu, ağın hızına bağlıdır. Hızlı bir bağlantı kullanıyorsanız daha az sayıda, daha büyük veri "parçaları" alırsınız. Yavaş bir bağlantı kullanıyorsanız daha fazla sayıda küçük parça alırsınız.

Baytları metne dönüştürmek istiyorsanız TextDecoder'u veya hedef tarayıcıları desteklediği takdirde daha yeni dönüştürme akışını kullanabilirsiniz:

const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

TextDecoderStream, tüm bu Uint8Array parçalarını alıp dizelere dönüştüren bir dönüştürme akışıdır.

Veriler geldikçe işlem yapmaya başlayabileceğiniz için akışlar çok kullanışlıdır. Örneğin, 100 "sonuç" içeren bir liste alıyorsanız 100'ün tamamını beklemek yerine ilk sonucu alır almaz gösterebilirsiniz.

Yanıt akışları hakkında bilgi verdikten sonra, bahsetmek istediğim yeni ve heyecan verici bir konu var: istek akışları.

Akış istek içerikleri

İsteklerde şu tür içerikler bulunabilir:

await fetch(url, {
  method: 'POST',
  body: requestBody,
});

Daha önce, isteği başlatabilmek için tüm gövdenin hazır olması gerekiyordu. Ancak Chromium 105'te artık kendi ReadableStream verilerinizi sağlayabilirsiniz:

function wait(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

const stream = new ReadableStream({
  async start(controller) {
    await wait(1000);
    controller.enqueue('This ');
    await wait(1000);
    controller.enqueue('is ');
    await wait(1000);
    controller.enqueue('a ');
    await wait(1000);
    controller.enqueue('slow ');
    await wait(1000);
    controller.enqueue('request.');
    controller.close();
  },
}).pipeThrough(new TextEncoderStream());

fetch(url, {
  method: 'POST',
  headers: {'Content-Type': 'text/plain'},
  body: stream,
  duplex: 'half',
});

Yukarıdaki kod, sunucuya "This is a slow request" ifadesini her kelime arasında bir saniye duraklayarak tek tek gönderir.

İstek gövdesinin her bir parçasının Uint8Array bayt olması gerekir. Bu nedenle, dönüşümü benim yerime yapmak için pipeThrough(new TextEncoderStream()) kullanıyorum.

Kısıtlamalar

Akış istekleri web için yeni bir güç olduğundan birkaç kısıtlamaya sahiptir:

Yarım çift yönlü mü?

Akışların bir istekte kullanılmasına izin vermek için duplex istek seçeneğinin 'half' olarak ayarlanması gerekir.

HTTP'nin az bilinen bir özelliği (standart davranış olup olmadığı kime sorduğunuza bağlıdır), isteği gönderirken yanıtı almaya başlayabilmenizdir. Ancak bu yöntem çok az bilindiği için sunucular tarafından iyi desteklenmez ve hiçbir tarayıcı tarafından desteklenmez.

Tarayıcılarda, sunucu daha erken yanıt gönderse bile istek gövdesi tamamen gönderilene kadar yanıt hiçbir zaman kullanılamaz. Bu, tüm tarayıcı getirme işlemleri için geçerlidir.

Bu varsayılan model "yarım dubleks" olarak bilinir. Ancak Deno'daki fetch gibi bazı uygulamalarda, akışlı getirmeler için varsayılan olarak "tam çift yönlü" ayarlanır. Bu durumda, yanıt istek tamamlanmadan önce kullanılabilir hale gelebilir.

Bu nedenle, bu uyumluluk sorununu gidermek için tarayıcılarda, yayın gövdesi içeren isteklerde duplex: 'half' belirtilmesi gerekir.

Gelecekte duplex: 'full', yayın ve yayın dışı istekler için tarayıcılarda desteklenebilir.

Bu sırada, çift yönlü iletişime en yakın yöntem, bir akış isteğiyle bir getirme işlemi yapmak ve ardından akış yanıtını almak için başka bir getirme işlemi yapmaktır. Sunucunun, bu iki isteği ilişkilendirmek için bir yönteme (ör. URL'deki bir kimlik) ihtiyacı vardır. Demo bu şekilde çalışır.

Kısıtlanmış yönlendirmeler

Bazı HTTP yönlendirme biçimleri, tarayıcının istek gövdesini başka bir URL'ye yeniden göndermesini gerektirir. Tarayıcının bunu desteklemesi için aktarımın içeriğini arabelleğe alması gerekir. Bu da amacın dışına çıkmak anlamına gelir. Bu nedenle, tarayıcı bunu yapmaz.

Bunun yerine, istekte akış gövdesi varsa ve yanıt 303 dışında bir HTTP yönlendirmesiyse getirme reddedilir ve yönlendirme izlenmez.

Yöntemi açıkça GET olarak değiştirip istek gövdesini attıkları için 303 yönlendirmelerine izin verilir.

CORS gerektirir ve ön uç uçuş tetiklenir

Akış isteklerinde gövde bulunur ancak Content-Length üstbilgisi yoktur. Bu yeni bir istek türü olduğundan CORS gereklidir ve bu istekler her zaman ön uç uçuş işlemini tetikler.

Akış no-cors isteklerine izin verilmez.

HTTP/1.x'te çalışmaz

Bağlantı HTTP/1.x ise getirme işlemi reddedilir.

Bunun nedeni, HTTP/1.1 kurallarına göre istek ve yanıt gövdelerinin, diğer tarafın ne kadar veri alacağını bilmesi için bir Content-Length başlığı göndermesi veya mesajın biçimini parçalara ayrılmış kodlama kullanacak şekilde değiştirmesi gerekmesidir. Parçalara ayırma kodlamasında, her biri kendi içerik uzunluğuna sahip parçalara ayrılır.

Parçalara ayırma kodlaması, HTTP/1.1 yanıtları söz konusu olduğunda oldukça yaygındır ancak istekler söz konusu olduğunda çok nadirdir. Bu nedenle, uyumluluk riski çok yüksektir.

Olası sorunlar

Bu, yeni ve günümüzde internette yeterince kullanılmayan bir özelliktir. Dikkat etmeniz gereken bazı sorunlar aşağıda açıklanmıştır:

Sunucu tarafında uyumsuzluk

Bazı uygulama sunucuları, akış isteklerini desteklemez ve isteğin tamamını almadan önce hiçbirini görmenize izin vermez. Bu da akış özelliğinin amacını ortadan kaldırır. Bunun yerine, NodeJS veya Deno gibi akış özelliğini destekleyen bir uygulama sunucusu kullanın.

Ancak henüz işiniz bitmedi. NodeJS gibi uygulama sunucusu genellikle başka bir sunucunun (genellikle "ön uç sunucu" olarak adlandırılır) arkasında bulunur. Bu sunucu da bir CDN'nin arkasında olabilir. Bu sunucular isteği zincirdeki bir sonraki sunucuya vermeden önce arabelleğe almaya karar verirse istek aktarma avantajını kaybedersiniz.

Kontrolünüz dışındaki uyumsuzluk

Bu özellik yalnızca HTTPS üzerinden çalıştığından, sizinle kullanıcı arasında proxy'ler olması konusunda endişelenmenize gerek yoktur. Ancak kullanıcı, kendi cihazında proxy çalıştırıyor olabilir. Bazı internet koruma yazılımları, tarayıcı ile ağ arasında geçen her şeyi izleyebilmek için bunu yapar. Bu yazılımların istek gövdelerini arabelleğe aldığı durumlar da olabilir.

Bu duruma karşı korunmak istiyorsanız yukarıdaki demoya benzer bir "özellik testi" oluşturabilirsiniz. Bu testte, akışı kapatmadan bazı verileri yayınlamayı denersiniz. Sunucu verileri alırsa farklı bir getirme işlemiyle yanıt verebilir. Bu işlem tamamlandığında, istemcinin akış isteklerini uçtan uca desteklediğini anlayabilirsiniz.

Özellik algılama

const supportsRequestStreams = (() => {
  let duplexAccessed = false;

  const hasContentType = new Request('', {
    body: new ReadableStream(),
    method: 'POST',
    get duplex() {
      duplexAccessed = true;
      return 'half';
    },
  }).headers.has('Content-Type');

  return duplexAccessed && !hasContentType;
})();

if (supportsRequestStreams) {
  // …
} else {
  // …
}

Özellik algılama özelliğinin işleyiş şeklini merak ediyorsanız aşağıdakileri inceleyebilirsiniz:

Tarayıcı belirli bir body türünü desteklemiyorsa nesnede toString()'yi çağırır ve sonucu gövde olarak kullanır. Bu nedenle, tarayıcı istek akışlarını desteklemiyorsa istek gövdesi "[object ReadableStream]" dizesi olur. Bir dize gövde olarak kullanıldığında Content-Type başlığı uygun şekilde text/plain;charset=UTF-8 olarak ayarlanır. Bu başlık ayarlanmışsa tarayıcı, istek nesnelerindeki akışları desteklemediğini anlarız ve erken çıkabiliriz.

Safari, istek nesnelerindeki akışları destekler ancak bunların fetch ile kullanılmasına izin vermez. Bu nedenle, Safari'nin şu anda desteklemediği duplex seçeneği test edilir.

Yazılabilir akışlarla kullanma

Bazen WritableStream'niz olduğunda akışlarla çalışmak daha kolaydır. Bunu, "identity" akışı kullanarak yapabilirsiniz. "identity" akışı, yazılabilir ucuna iletilen her şeyi alıp okunabilir ucuna gönderen bir okunabilir/yazılabilir çifttir. Argüman olmadan bir TransformStream oluşturarak bunlardan birini oluşturabilirsiniz:

const {readable, writable} = new TransformStream();

const responsePromise = fetch(url, {
  method: 'POST',
  body: readable,
});

Artık, yazılabilir akışa gönderdiğiniz her şey isteğin bir parçası olacak. Bu sayede, akışları birlikte derleyebilirsiniz. Örneğin, verilerin bir URL'den getirildiği, sıkıştırıldığı ve başka bir URL'ye gönderildiği aptalca bir örnek aşağıda verilmiştir:

// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();

// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);

// Post to url2:
await fetch(url2, {
  method: 'POST',
  body: readable,
});

Yukarıdaki örnekte, gzip kullanarak rastgele verileri sıkıştırmak için sıkıştırma akışları kullanılmaktadır.