Getirme API'si ile akış istekleri

Jake Archibald
Jake Archibald

Chromium 105'ten itibaren, Streams API'yi kullanarak gövdenin tamamı kullanıma sunulmadan önce bir istek başlatabilirsiniz.

Bunu şu amaçlarla kullanabilirsiniz:

  • Sunucuyu ısıt. Başka bir deyişle, isteği kullanıcı bir metin giriş alanına odaklanıp tüm üstbilgileri ortadan kaldırdıktan sonra başlatabilir ve girdiği verileri göndermeden önce kullanıcının "gönder" düğmesine basmasını bekleyebilirsiniz.
  • İstemcide oluşturulan ses, video veya giriş verileri gibi verileri kademeli olarak gönderin.
  • Web yuvalarını HTTP/2 veya HTTP/3 üzerinden yeniden oluşturun.

Ancak bu alt düzey bir web platformu özelliği olduğundan, benim fikirlerimle sınırlandırmayın. Canlı yayın isteğinde bulunmak için çok daha heyecan verici bir kullanım alanı düşünebilirsiniz.

Demo

Bu, kullanıcıdan sunucuya nasıl veri akışı yapabileceğinizi ve gerçek zamanlı olarak işlenebilecek verileri nasıl geri göndereceğinizi gösterir.

Evet, bu en gerçekçi örnek değil. Ben sadece basit tutmak istedim, tamam mı?

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

Yayınların heyecan verici maceralarında

Yanıt akışları bir süredir tüm modern tarayıcılarda kullanılabiliyor. Sunucudan gelen bir yanıtın bazı bölümlerine erişmenize olanak tanırlar:

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, bir Uint8Array bayttır. Aldığınız dizi sayısı ve dizilerin boyutu, ağın hızına bağlıdır. Hızlı bir bağlantıdaysanız daha az, daha büyük "parçalar" alırsınız. Bağlantınız yavaşsa daha fazla, daha küçük parçalar alırsınız.

Baytları metne dönüştürmek istiyorsanız TextDecoder veya hedef tarayıcılarınız destekliyorsa daha yeni dönüşüm 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üşüm akışıdır.

Akışlar harikadır, çünkü gelen verilere göre işlem yapmaya başlayabilirsiniz. Örneğin, 100 "sonuç" içeren bir liste alıyorsanız, 100 sonucun tamamını beklemek yerine, alır almaz ilk sonucu görüntüleyebilirsiniz.

Konu yanıt akışları. Bahsetmek istediğim en heyecan verici konu ise istek akışları.

Akış istek gövdeleri

İsteklerin gövdeleri olabilir:

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

Önceden, isteği başlatabilmek için tüm vücudun hazır olması gerekiyordu, ancak artık Chromium 105'te 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 komut, sunucuya her seferinde bir kelime olacak şekilde, her kelime arasında bir saniyelik duraklama olacak şekilde "Bu yavaş bir istek" gönderir.

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

Kısıtlamalar

Akış istekleri web için yeni bir güçtür, dolayısıyla bazı kısıtlamalarla gelirler:

Yarım dubleks mi?

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

HTTP'nin pek bilinmeyen bir özelliği (ancak bunun standart bir davranış olup olmadığı, isteği gönderen kişiye göre değişir) yanıtı almaya başlayabilmenizdir. Ancak, sunucular tarafından iyi desteklenmediği ve hiçbir tarayıcı tarafından desteklenmediği konusunda çok az bilinen bir gerçektir.

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

Bu varsayılan kalıp "yarım çift yönlü" olarak bilinir. Bununla birlikte, bazı uygulamalarda (ör. Deno'daki fetch), akış getirmeleri için varsayılan olarak "tam çift yönlü" ayarı kullanılır. Bu da, yanıtın istek tamamlanmadan hazır olabileceği anlamına gelir.

Dolayısıyla, bu uyumluluk sorununa çözüm bulmak için tarayıcılarda, akış gövdesine sahip isteklerde duplex: 'half' belirtilmesi gerekir.

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

Bu sırada, çift yönlü iletişimin bir sonraki en iyi yöntemi, akış isteğiyle bir getirme işlemi yapmak, ardından akış yanıtını almak için başka bir getirme işlemi yapmaktır. Sunucunun bu iki isteği ilişkilendirebilmesi için URL'deki kimlik gibi bir yönteme ihtiyacı vardır. Demonun çalışma şekli budur.

Kısıtlanmış yönlendirmeler

Bazı HTTP yönlendirmesi biçimleri, tarayıcının isteğin gövdesini başka bir URL'ye yeniden göndermesini gerektirir. Bunu desteklemek için tarayıcının akış içeriğini arabelleğe alması gerekir. Bu da bir şekilde noktayı etkisizleştirir ve bunu yapmaz.

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

303 yönlendirmelerine, yöntemi açık bir şekilde GET olarak değiştirdikleri ve istek gövdesini sildikleri için izin verilir.

CORS gerektirir ve bir ön kontrolü tetikler

Yayın isteklerinin bir gövdesi vardır ancak Content-Length başlığı yoktur. Bu yeni bir istek türü olduğundan CORS gerekir ve bu istekler her zaman ön kontrolü tetikler.

Akış no-cors isteklerine izin verilmez.

HTTP/1.x üzerinde ç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 üstbilgisi göndermesi veya iletinin biçimini parçalı kodlama kullanacak şekilde değiştirmesidir. Parçalı kodlamada gövde, her biri kendi içerik uzunluğuna sahip parçalara bölünür.

Öbek tabanlı kodlama, HTTP/1.1 yanıtları söz konusu olduğunda oldukça yaygındır ancak istekler söz konusu olduğunda çok nadir görülür. Bu nedenle, çok büyük bir uyumluluk riski oluşturur.

Olası sorunlar

Bu yeni ve günümüzde internette az kullanılan bir özelliktir. Dikkat etmeniz gereken bazı sorunlar aşağıda belirtilmiştir:

Sunucu tarafında uyumsuzluk

Bazı uygulama sunucuları akış isteklerini desteklemez. Bunun yerine, herhangi bir isteği görmenize izin vermeden önce isteğin tamamının alınmasını beklersiniz. Bu durumda hak talebi geçersizdir. Bunun yerine, NodeJS veya Deno gibi akışı destekleyen bir uygulama sunucusu kullanın.

Ancak daha hevesli değilsiniz! NodeJS gibi uygulama sunucusu, genellikle "ön uç sunucusu" olarak adlandırılan ve bir CDN'nin arkasında yer alan başka bir sunucunun arkasında bulunur. Bu kişilerden herhangi biri isteği zincirdeki bir sonraki sunucuya vermeden önce arabelleğe almaya karar verirse istek akışı avantajını kaybedersiniz.

Sizin kontrolünüz dışında uyumsuzluk

Bu özellik yalnızca HTTPS üzerinden çalıştığından, sizinle kullanıcı arasındaki proxy'ler konusunda endişelenmenize gerek yoktur; ancak kullanıcı, makinesinde bir proxy çalıştırıyor olabilir. Bazı internet koruma yazılımları bunu, tarayıcı ile ağ arasındaki her şeyi izlemesine olanak tanımak için yapar ve bu yazılımın gövdeleri arabelleğe aldığı durumlar olabilir.

Buna karşı kendinizi korumak istiyorsanız yukarıdaki demoya benzer bir "özellik testi" oluşturabilirsiniz. Bu testte, akışı kapatmadan bazı verilerin akışını gerçekleştirmeyi deneyebilirsiniz. Sunucu verileri alırsa farklı bir getirme işlemiyle yanıt verebilir. Bu durumda istemcinin akış isteklerini uçtan uca desteklediğini anlarsınız.

Ö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 {
  // …
}

Merak ediyorsanız özellik algılamanın nasıl çalıştığı aşağıda açıklanmıştır:

Tarayıcı belirli bir body türünü desteklemiyorsa nesnede toString() işlevini çağırır ve sonucu gövde olarak kullanır. Dolayısıyla, tarayıcı istek akışlarını desteklemiyorsa isteğin gövde metni "[object ReadableStream]" dizesi haline gelir. Bir dize, gövde olarak kullanıldığında Content-Type üstbilgisini text/plain;charset=UTF-8 değerine kolayca ayarlar. Dolayısıyla, bu başlık ayarlanırsa tarayıcının istek nesnelerinde akışları desteklediğini biliriz ve erkenden çıkış yapabiliriz.

Safari, istek nesnelerinde 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

WritableStream sahibi olduğunuzda canlı yayınlarla çalışmak bazen daha kolay olur. Bu işlemi, yazılabilir sonuna iletilen her şeyi alıp okunabilir sonuna gönderen okunabilir/yazılabilir bir çift olan "kimlik" akışı kullanarak yapabilirsiniz. Herhangi bir bağımsız değişken olmadan TransformStream oluşturarak şunlardan 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ı olur. Bu sayede yayınları birlikte oluşturabilirsiniz. Örneğin, verilerin bir URL'den getirildiği, sıkıştırıldığı ve başka bir URL'ye gönderildiği saçma bir örneği aşağıda görebilirsiniz:

// 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 kullanılarak rastgele verileri sıkıştırmak için sıkıştırma akışları kullanılmıştır.