Kể từ Chromium 105, bạn có thể bắt đầu một yêu cầu trước khi có toàn bộ nội dung bằng cách sử dụng Streams API.
Bạn có thể sử dụng tính năng này để:
- Làm ấm máy chủ. Nói cách khác, bạn có thể bắt đầu yêu cầu sau khi người dùng đặt tiêu điểm vào trường nhập văn bản và loại bỏ tất cả tiêu đề, sau đó chờ đến khi người dùng nhấn vào nút "gửi" trước khi gửi dữ liệu mà họ đã nhập.
- Dần dần gửi dữ liệu được tạo trên máy khách, chẳng hạn như âm thanh, video hoặc dữ liệu đầu vào.
- Tạo lại ổ cắm web qua HTTP/2 hoặc HTTP/3.
Tuy nhiên, vì đây là một tính năng cấp thấp của nền tảng web, nên đừng giới hạn bản thân trong những ý tưởng của tôi. Có thể bạn có thể nghĩ ra một trường hợp sử dụng thú vị hơn nhiều cho việc truyền phát yêu cầu.
Bản minh hoạ
Hình này cho thấy cách bạn có thể truyền trực tuyến dữ liệu từ người dùng đến máy chủ và gửi lại dữ liệu có thể được xử lý theo thời gian thực.
Vâng, đây không phải là ví dụ sáng tạo nhất, tôi chỉ muốn đơn giản hoá vấn đề, được chứ?
Dù sao, cách hoạt động của tính năng này như thế nào?
Trước đây, trong các cuộc phiêu lưu thú vị về luồng tìm nạp
Luồng phản hồi đã có trong tất cả trình duyệt hiện đại từ lâu. Các phương thức này cho phép bạn truy cập vào các phần của phản hồi khi chúng đến từ máy chủ:
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');
Mỗi value
là một Uint8Array
gồm các byte.
Số lượng mảng bạn nhận được và kích thước của mảng phụ thuộc vào tốc độ mạng.
Nếu đang dùng kết nối nhanh, bạn sẽ nhận được ít "mảng" dữ liệu hơn nhưng lớn hơn.
Nếu đang dùng kết nối chậm, bạn sẽ nhận được nhiều phần nhỏ hơn.
Nếu muốn chuyển đổi các byte thành văn bản, bạn có thể sử dụng TextDecoder
hoặc luồng biến đổi mới hơn nếu các trình duyệt mục tiêu của bạn hỗ trợ luồng này:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
là một luồng biến đổi lấy tất cả các đoạn Uint8Array
đó và chuyển đổi chúng thành chuỗi.
Luồng rất hữu ích vì bạn có thể bắt đầu xử lý dữ liệu khi dữ liệu đến. Ví dụ: nếu nhận được danh sách gồm 100 "kết quả", bạn có thể hiển thị kết quả đầu tiên ngay khi nhận được, thay vì chờ tất cả 100 kết quả.
Dù sao, đó là luồng phản hồi, điều mới mẻ và thú vị mà tôi muốn nói đến là luồng yêu cầu.
Nội dung yêu cầu phát trực tuyến
Yêu cầu có thể có nội dung:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Trước đây, bạn cần phải chuẩn bị sẵn toàn bộ nội dung trước khi có thể bắt đầu yêu cầu, nhưng giờ đây, trong Chromium 105, bạn có thể cung cấp ReadableStream
dữ liệu của riêng mình:
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',
});
Mã trên sẽ gửi "This is a slow request" (Đây là một yêu cầu chậm) đến máy chủ, mỗi từ một lần, với một giây tạm dừng giữa mỗi từ.
Mỗi phần của nội dung yêu cầu cần phải là Uint8Array
byte, vì vậy, tôi sẽ sử dụng pipeThrough(new TextEncoderStream())
để thực hiện việc chuyển đổi.
Quy định hạn chế
Yêu cầu truyền trực tuyến là một tính năng mới cho web, vì vậy, chúng có một số hạn chế:
Bán song công?
Để cho phép sử dụng luồng trong một yêu cầu, bạn cần đặt tuỳ chọn yêu cầu duplex
thành 'half'
.
Một tính năng ít được biết đến của HTTP (mặc dù việc đây có phải là hành vi chuẩn hay không còn tuỳ thuộc vào người bạn hỏi) là bạn có thể bắt đầu nhận được phản hồi trong khi vẫn đang gửi yêu cầu. Tuy nhiên, phương thức này ít được biết đến nên không được máy chủ hỗ trợ tốt và không được trình duyệt nào hỗ trợ.
Trong trình duyệt, phản hồi sẽ không bao giờ xuất hiện cho đến khi toàn bộ nội dung yêu cầu được gửi, ngay cả khi máy chủ gửi phản hồi sớm hơn. Điều này đúng với tất cả các lần tìm nạp của trình duyệt.
Mẫu mặc định này được gọi là "half duplex" (bán song công).
Tuy nhiên, một số cách triển khai, chẳng hạn như fetch
trong Deno, mặc định là "full duplex" (kỹ thuật truyền dữ liệu hai chiều) để tìm nạp trực tuyến, nghĩa là phản hồi có thể xuất hiện trước khi yêu cầu hoàn tất.
Vì vậy, để giải quyết vấn đề về khả năng tương thích này, trong trình duyệt, bạn cần chỉ định duplex: 'half'
trên các yêu cầu có phần nội dung luồng.
Trong tương lai, duplex: 'full'
có thể được hỗ trợ trong trình duyệt cho các yêu cầu phát trực tuyến và không phát trực tuyến.
Trong thời gian chờ đợi, điều tốt nhất tiếp theo để giao tiếp song công là thực hiện một lần tìm nạp bằng yêu cầu truyền trực tuyến, sau đó thực hiện một lần tìm nạp khác để nhận phản hồi truyền trực tuyến. Máy chủ sẽ cần một số cách để liên kết hai yêu cầu này, chẳng hạn như mã nhận dạng trong URL. Đó là cách hoạt động của bản minh hoạ.
Lượt chuyển hướng bị hạn chế
Một số hình thức chuyển hướng HTTP yêu cầu trình duyệt gửi lại nội dung của yêu cầu đến một URL khác. Để hỗ trợ điều này, trình duyệt sẽ phải lưu nội dung của luồng vào bộ đệm, điều này sẽ làm mất đi ý nghĩa của luồng, vì vậy, trình duyệt sẽ không làm như vậy.
Thay vào đó, nếu yêu cầu có nội dung truyền trực tuyến và phản hồi là một lệnh chuyển hướng HTTP khác với 303, thì lệnh tìm nạp sẽ từ chối và lệnh chuyển hướng sẽ không được thực hiện.
Bạn được phép sử dụng lệnh chuyển hướng 303 vì các lệnh này thay đổi rõ ràng phương thức thành GET
và loại bỏ nội dung yêu cầu.
Yêu cầu CORS và kích hoạt quy trình kiểm tra trước
Yêu cầu truyền trực tuyến có nội dung nhưng không có tiêu đề Content-Length
.
Đó là một loại yêu cầu mới, vì vậy, bạn phải có CORS và các yêu cầu này luôn kích hoạt quy trình kiểm tra trước.
Không cho phép truyền trực tuyến yêu cầu no-cors
.
Không hoạt động trên HTTP/1.x
Lệnh tìm nạp sẽ bị từ chối nếu kết nối là HTTP/1.x.
Lý do là theo quy tắc HTTP/1.1, phần nội dung yêu cầu và phản hồi cần gửi tiêu đề Content-Length
để bên kia biết lượng dữ liệu sẽ nhận được hoặc thay đổi định dạng của thông báo để sử dụng mã hoá theo khối. Với phương thức mã hoá theo từng phần, nội dung sẽ được chia thành các phần, mỗi phần có độ dài nội dung riêng.
Việc mã hoá theo từng phần khá phổ biến đối với phản hồi HTTP/1.1, nhưng rất hiếm khi xảy ra đối với yêu cầu, vì vậy, việc này có quá nhiều rủi ro về khả năng tương thích.
Các vấn đề tiềm ẩn
Đây là một tính năng mới và chưa được sử dụng nhiều trên Internet hiện nay. Dưới đây là một số vấn đề cần lưu ý:
Không tương thích ở phía máy chủ
Một số máy chủ ứng dụng không hỗ trợ yêu cầu truyền trực tuyến, thay vào đó, hãy đợi nhận được toàn bộ yêu cầu trước khi cho phép bạn xem bất kỳ yêu cầu nào, điều này có thể làm mất đi ý nghĩa của yêu cầu. Thay vào đó, hãy sử dụng máy chủ ứng dụng hỗ trợ tính năng truyền trực tuyến, chẳng hạn như NodeJS hoặc Deno.
Nhưng bạn vẫn chưa thoát khỏi rừng! Máy chủ ứng dụng, chẳng hạn như NodeJS, thường nằm sau một máy chủ khác, thường được gọi là "máy chủ giao diện người dùng", và máy chủ này có thể nằm sau một CDN. Nếu bất kỳ máy chủ nào trong số đó quyết định lưu yêu cầu vào bộ đệm trước khi chuyển yêu cầu đó đến máy chủ tiếp theo trong chuỗi, thì bạn sẽ mất lợi ích của tính năng truyền trực tuyến yêu cầu.
Sự không tương thích nằm ngoài tầm kiểm soát của bạn
Vì tính năng này chỉ hoạt động qua HTTPS, nên bạn không cần lo lắng về proxy giữa bạn và người dùng, nhưng người dùng có thể đang chạy proxy trên máy của họ. Một số phần mềm bảo vệ Internet thực hiện việc này để cho phép phần mềm theo dõi mọi thứ diễn ra giữa trình duyệt và mạng. Trong một số trường hợp, phần mềm này có thể lưu vào bộ đệm nội dung yêu cầu.
Nếu muốn ngăn chặn điều này, bạn có thể tạo một "kiểm thử tính năng" tương tự như màn hình minh hoạ ở trên, trong đó bạn cố gắng truyền trực tuyến một số dữ liệu mà không đóng luồng. Nếu nhận được dữ liệu, máy chủ có thể phản hồi thông qua một lệnh tìm nạp khác. Khi điều này xảy ra, bạn sẽ biết rằng ứng dụng hỗ trợ các yêu cầu truyền trực tuyến từ đầu đến cuối.
Phát hiện tính năng
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 {
// …
}
Nếu bạn tò mò, sau đây là cách hoạt động của tính năng phát hiện tính năng:
Nếu không hỗ trợ một loại body
cụ thể, trình duyệt sẽ gọi toString()
trên đối tượng và sử dụng kết quả làm nội dung.
Vì vậy, nếu trình duyệt không hỗ trợ luồng yêu cầu, thì nội dung yêu cầu sẽ trở thành chuỗi "[object ReadableStream]"
.
Khi một chuỗi được dùng làm nội dung, chuỗi đó sẽ đặt tiêu đề Content-Type
thành text/plain;charset=UTF-8
một cách thuận tiện.
Vì vậy, nếu tiêu đề đó được đặt, thì chúng ta biết rằng trình duyệt không hỗ trợ luồng trong đối tượng yêu cầu và chúng ta có thể thoát sớm.
Safari có hỗ trợ luồng trong đối tượng yêu cầu, nhưng không cho phép sử dụng luồng với fetch
, vì vậy, tuỳ chọn duplex
được kiểm thử, mà Safari hiện không hỗ trợ.
Sử dụng với luồng có thể ghi
Đôi khi, bạn sẽ dễ dàng làm việc với các luồng hơn khi có WritableStream
.
Bạn có thể thực hiện việc này bằng cách sử dụng luồng "danh tính". Đây là một cặp có thể đọc/ghi, nhận mọi nội dung được truyền đến đầu ghi và gửi nội dung đó đến đầu đọc.
Bạn có thể tạo một trong các tệp này bằng cách tạo TransformStream
mà không cần bất kỳ đối số nào:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Giờ đây, mọi nội dung bạn gửi đến luồng có thể ghi sẽ là một phần của yêu cầu. Điều này cho phép bạn kết hợp các luồng với nhau. Ví dụ: sau đây là một ví dụ ngớ ngẩn về việc dữ liệu được tìm nạp từ một URL, nén và gửi đến một URL khác:
// 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,
});
Ví dụ trên sử dụng luồng nén để nén dữ liệu tuỳ ý bằng gzip.