В Chromium 105 вы можете запустить запрос до того, как у вас будет доступно все тело, используя Streams API .
Вы можете использовать это, чтобы:
- Прогрейте сервер. Другими словами, вы можете запустить запрос, как только пользователь сфокусирует поле ввода текста, и убрать все заголовки, а затем подождать, пока пользователь не нажмет «отправить», прежде чем отправлять введенные данные.
- Постепенно отправляйте данные, сгенерированные на клиенте, например аудио, видео или входные данные.
- Воссоздайте веб-сокеты через HTTP/2 или HTTP/3.
Но поскольку это функция веб-платформы низкого уровня, не ограничивайтесь моими идеями. Возможно, вы сможете придумать гораздо более интересный вариант использования потоковой передачи запросов.
Демо
Здесь показано, как можно передавать данные от пользователя на сервер и отправлять данные обратно, которые можно обрабатывать в режиме реального времени.
Да, ок, это не самый творческий пример, я просто хотел, чтобы все было проще, ладно?
В любом случае, как это работает?
Ранее об захватывающих приключениях fetch стримов
Потоки ответов уже давно доступны во всех современных браузерах. Они позволяют вам получить доступ к частям ответа по мере их поступления с сервера:
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');
Каждое value
представляет собой Uint8Array
байтов. Количество получаемых массивов и размер массивов зависят от скорости сети. Если у вас быстрое соединение, вы получите меньше «кусков» данных, но более крупных. Если у вас медленное соединение, вы получите больше кусков меньшего размера.
Если вы хотите преобразовать байты в текст, вы можете использовать TextDecoder
или более новый поток преобразования, если ваши целевые браузеры поддерживают его :
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
— это поток преобразования, который захватывает все фрагменты Uint8Array
и преобразует их в строки.
Потоки — это здорово, поскольку вы можете начать работать с данными по мере их поступления. Например, если вы получаете список из 100 «результатов», вы можете отобразить первый результат сразу после его получения, а не ждать всех 100.
В любом случае, это потоки ответов, и интересная новая вещь, о которой я хотел поговорить, — это потоки запросов.
Тела потоковой передачи запросов
Запросы могут иметь тела:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Раньше вам требовалось, чтобы все тело было готово к работе, прежде чем вы могли запустить запрос, но теперь в Chromium 105 вы можете предоставить свой собственный ReadableStream
данных:
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',
});
Вышеупомянутое сообщение отправит на сервер сообщение «Это медленный запрос» по одному слову за раз с паузой в одну секунду между каждым словом.
Каждый фрагмент тела запроса должен представлять собой Uint8Array
байтов, поэтому я использую pipeThrough(new TextEncoderStream())
для выполнения преобразования.
Ограничения
Потоковые запросы — это новая возможность для Интернета, поэтому они имеют несколько ограничений:
Полудуплекс?
Чтобы разрешить использование потоков в запросе, для параметра duplex
запроса необходимо установить значение 'half'
.
Малоизвестная особенность HTTP (хотя является ли это стандартным поведением, зависит от того, кого вы спрашиваете) заключается в том, что вы можете начать получать ответ, пока еще отправляете запрос. Однако он настолько малоизвестен, что плохо поддерживается серверами и не поддерживается ни одним браузером.
В браузерах ответ никогда не становится доступным до тех пор, пока тело запроса не будет полностью отправлено, даже если сервер отправит ответ раньше. Это справедливо для всех операций загрузки браузера.
Этот шаблон по умолчанию известен как «полудуплекс». Однако некоторые реализации, такие как fetch
в Deno , по умолчанию используют «полнодуплексный режим» для потоковой выборки, что означает, что ответ может стать доступным до завершения запроса.
Итак, чтобы обойти эту проблему совместимости, в браузерах необходимо указать duplex: 'half'
для запросов, имеющих тело потока.
В будущем duplex: 'full'
может поддерживаться в браузерах для потоковых и непотоковых запросов.
Между тем, следующий лучший вариант дуплексной связи — выполнить одну выборку с запросом потоковой передачи, а затем выполнить еще одну выборку для получения ответа потоковой передачи. Серверу потребуется какой-то способ связать эти два запроса, например идентификатор в URL-адресе. Вот как работает демо .
Ограниченные перенаправления
Некоторые формы перенаправления HTTP требуют, чтобы браузер повторно отправил тело запроса на другой URL-адрес. Чтобы поддержать это, браузеру придется буферизовать содержимое потока, что в некотором роде противоречит сути, поэтому он этого не делает.
Вместо этого, если запрос имеет потоковое тело, а ответ представляет собой перенаправление HTTP, отличное от 303, выборка будет отклонена, и перенаправление не будет выполнено.
Перенаправления 303 разрешены, поскольку они явно меняют метод на GET
и отбрасывают тело запроса.
Требуется CORS и запускает предполетную проверку
Запросы потоковой передачи имеют тело, но не имеют заголовка Content-Length
. Это новый тип запроса, поэтому требуется CORS, и эти запросы всегда запускают предварительную проверку.
Потоковая передача запросов no-cors
не допускается.
Не работает на HTTP/1.x
Извлечение будет отклонено, если соединение HTTP/1.x.
Это связано с тем, что в соответствии с правилами HTTP/1.1 тела запроса и ответа должны либо отправить заголовок Content-Length
, чтобы другая сторона знала, сколько данных она получит, либо изменить формат сообщения для использования фрагментированного кодирования . При фрагментированном кодировании тело разбивается на части, каждая из которых имеет свою длину содержимого.
Кодирование фрагментов довольно распространено, когда речь идет об ответах HTTP/1.1, но очень редко, когда дело касается запросов , поэтому это слишком большой риск совместимости.
Потенциальные проблемы
Это новая функция, которая сегодня мало используется в Интернете. Вот некоторые проблемы, на которые следует обратить внимание:
Несовместимость на стороне сервера
Некоторые серверы приложений не поддерживают потоковые запросы и вместо этого ждут получения полного запроса, прежде чем позволить вам увидеть его, что в некотором роде противоречит сути. Вместо этого используйте сервер приложений, поддерживающий потоковую передачу, например NodeJS или Deno .
Но вы еще не вышли из леса! Сервер приложений, такой как NodeJS, обычно находится за другим сервером, часто называемым «интерфейсным сервером», который, в свою очередь, может находиться за CDN. Если кто-то из них решит буферизовать запрос перед его передачей следующему серверу в цепочке, вы потеряете преимущество потоковой передачи запросов.
Несовместимость вне вашего контроля
Поскольку эта функция работает только через HTTPS, вам не нужно беспокоиться о прокси-серверах между вами и пользователем, но пользователь может использовать прокси-сервер на своем компьютере. Некоторые программы интернет-защиты делают это, чтобы отслеживать все, что происходит между браузером и сетью, и могут быть случаи, когда это программное обеспечение буферизует тела запросов.
Если вы хотите защититься от этого, вы можете создать «проверку функций», аналогичную приведенной выше демонстрации , где вы попытаетесь передать некоторые данные в потоковом режиме, не закрывая поток. Если сервер получает данные, он может ответить другой выборкой. Как только это произойдет, вы будете знать, что клиент поддерживает сквозные потоковые запросы.
Обнаружение функций
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 {
// …
}
Если вам интересно, вот как работает обнаружение функции:
Если браузер не поддерживает определенный тип body
, он вызывает toString()
для объекта и использует результат в качестве тела. Итак, если браузер не поддерживает потоки запросов, телом запроса становится строка "[object ReadableStream]"
. Когда строка используется в качестве тела, для удобства заголовку Content-Type
присваивается значение text/plain;charset=UTF-8
. Итак, если этот заголовок установлен, мы знаем, что браузер не поддерживает потоки в объектах запроса, и мы можем выйти раньше.
Safari поддерживает потоки в объектах запроса, но не позволяет использовать их с fetch
, поэтому тестируется вариант duplex
, который Safari в настоящее время не поддерживает.
Использование с записываемыми потоками
Иногда с потоками проще работать, если у вас есть WritableStream
. Вы можете сделать это, используя «идентификационный» поток, который представляет собой пару, доступную для чтения и записи, которая принимает все, что передается на его записываемый конец, и отправляет его на читаемый конец. Вы можете создать один из них, создав TransformStream
без каких-либо аргументов:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Теперь все, что вы отправляете в записываемый поток, будет частью запроса. Это позволяет вам составлять потоки вместе. Например, вот глупый пример, когда данные извлекаются из одного URL-адреса, сжимаются и отправляются на другой URL-адрес:
// 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,
});
В приведенном выше примере потоки сжатия используются для сжатия произвольных данных с помощью gzip.