Yêu cầu truyền trực tuyến bằng API tìm nạp

Jake Archibald
Jake Archibald

Trên 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 API Luồng.

Bạn có thể sử dụng báo cáo này để:

  • Khởi động 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à di chuyển tất cả tiêu đề, sau đó đợi cho đến khi người dùng nhấn "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 ứng dụng, chẳng hạn như âm thanh, video hoặc dữ liệu đầu vào.
  • Tạo lại cổng web qua HTTP/2 hoặc HTTP/3.

Tuy nhiên, vì đây là tính năng cấp thấp trên nền tảng web, nên bạn đừng nên giới hạn theo các ý tưởng của tôi. Có lẽ bạn có thể nghĩ đến một trường hợp sử dụng thú vị hơn nhiều để yêu cầu truyền trực tuyến.

Bản minh hoạ

Hình này minh hoạ 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, đó không phải là ví dụ kích thích trí tưởng tượng nhất, tôi chỉ muốn làm cho nó đơn giản thôi, được chứ?

Dù sao thì cách hoạt động ra sao?

Trước đây về những cuộc phiêu lưu thú vị khi tìm nạp luồng dữ liệu

Luồng phản hồi đã có sẵn trong tất cả các trình duyệt hiện đại từ một thời gian. Chúng 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 byte. Số lượng mảng bạn nhận được và kích thước của các mảng phụ thuộc vào tốc độ của mạng. Nếu đang có kết nối nhanh, bạn sẽ nhận được ít "khối" dữ liệu hơn. Nếu bạn đang có 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ó 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 (Transformer stream) nhận tất cả phần Uint8Array đó và chuyển đổi chúng thành chuỗi.

Luồng rất tuyệt vời vì bạn có thể bắt đầu hành động dựa trên dữ liệu khi chúng đến. Ví dụ: nếu bạn nhận được danh sách 100 'kết quả', bạn có thể hiển thị kết quả đầu tiên ngay khi bạn nhận được, thay vì đợi tất cả 100.

Dù sao thì đó là luồng phản hồi. Điều mới thú vị mà tôi muốn nói đến là luồng yêu cầu.

Nội dung yêu cầu truyền 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 toàn bộ cơ thể sẵn sàng để bắt đầu yêu cầu 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',
});

Thao tác trên sẽ gửi thông báo "Đây là yêu cầu chậm" đến máy chủ, mỗi từ một từ và tạm dừng một giây giữa mỗi từ.

Mỗi phần của nội dung yêu cầu cần phải là một Uint8Array byte, vì vậy tôi đang sử dụng pipeThrough(new TextEncoderStream()) để thực hiện chuyển đổi cho mình.

Quy định hạn chế

Yêu cầu phát trực tuyến là một sức mạnh mới cho web, vì vậy, yêu cầu này cũng có một số hạn chế như sau:

Một nửa duplex?

Để 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ù, liệu đây có phải là hành vi chuẩn hay không còn tuỳ thuộc vào người bạn yêu cầu) là bạn có thể bắt đầu nhận phản hồi trong khi vẫn 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 các máy chủ hỗ trợ tốt và không được bất kỳ trình duyệt nào hỗ trợ.

Trong trình duyệt, phản hồi không bao giờ có sẵn cho đến khi nội dung yêu cầu được gửi đầy đủ, ngay cả khi máy chủ gửi phản hồi sớm hơn. Điều này đúng với mọi quá trình tìm nạp trình duyệt.

Mẫu mặc định này được gọi là 'bán song công'. Tuy nhiên, một số phương pháp triển khai (chẳng hạn như fetch trong Deno) được đặt mặc định thành "full duplex" đối với các lượt tìm nạp theo luồng, nghĩa là phản hồi có thể có sẵ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' trong các yêu cầu có nội dung luồng.

Trong tương lai, duplex: 'full' có thể được hỗ trợ trong các trình duyệt dành cho các yêu cầu phát trực tuyến và không phải truyền trực tuyến.

Trong thời gian chờ đợi, điều tốt nhất tiếp theo đối với 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 cách nào đó để liên kết hai yêu cầu này, chẳng hạn như một mã nhận dạng trong URL. Đó là cách hoạt động của bản minh hoạ.

Lệnh 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 phần nội dung của yêu cầu đến một URL khác. Để hỗ trợ việc này, trình duyệt sẽ phải lưu vào bộ đệm nội dung của luồng, điều này không thể đánh bại điểm này, vì vậy trình duyệt không làm được việc đó.

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ông phải là 303, thì hoạt động tìm nạp sẽ từ chối và không tuân theo lệnh chuyển hướng.

Cho phép chuyển hướng 303 vì phương thức 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

Các yêu cầu truyền trực tuyến có phần nội dung, nhưng không có tiêu đề Content-Length. Đó là loại yêu cầu mới, vì vậy, bạn phải dùng CORS và những yêu cầu này sẽ luôn kích hoạt quy trình kiểm tra.

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

Hoạt động tìm nạp sẽ bị từ chối nếu kết nối là HTTP/1.x.

Điều này là do, theo các quy tắc HTTP/1.1, các nội dung yêu cầu và phản hồi cần phải gửi một tiêu đề Content-Length để phía 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á chia nhỏ. Với phương pháp mã hoá chia nhỏ, nội dung sẽ được chia thành các phần, mỗi phần có thời lượng nội dung riêng.

Phương pháp mã hoá phân đoạn khá phổ biến khi nói đến phản hồi HTTP/1.1, nhưng rất hiếm khi nói đến yêu cầu, vì vậy sẽ dẫn đến rủi ro về khả năng tương thích quá nhiều.

Các vấn đề tiềm ẩn

Đây là một tính năng mới và là một tính năng chưa được sử dụng trên Internet hiện nay. Dưới đây là một số vấn đề cần lưu ý:

Tình trạng không tương thích ở phía máy chủ

Một số máy chủ ứng dụng không hỗ trợ các yêu cầu truyền trực tuyến. Thay vào đó, hãy đợi cho đến khi nhận được toàn bộ yêu cầu rồi mới cho phép bạn xem bất kỳ yêu cầu nào trong số đó, vì điều này không khả thi. Thay vào đó, hãy dùng một máy chủ ứng dụng có hỗ trợ tính năng truyền trực tuyến, chẳng hạn như NodeJS hoặc Deno.

Tuy nhiên, bạn vẫn chưa thoát ra được! Máy chủ ứng dụng (chẳng hạn như NodeJS) thường nằm phía 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à có thể nằm sau một CDN. Nếu bất kỳ ai trong số đó quyết định lưu yêu cầu vào bộ đệm trước khi cung cấp cho máy chủ tiếp theo trong chuỗi, thì bạn sẽ không được hưởng lợi ích của việc truyền trực tuyến yêu cầu.

Tính 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 phải 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 giám sát mọi thứ diễn ra giữa trình duyệt và mạng, và có thể có trường hợp phần mềm này lưu các nội dung yêu cầu vào bộ đệm.

Nếu muốn ngăn chặn điều này, bạn có thể tạo một "thử nghiệm tính năng" tương tự như bản minh hoạ ở trên, trong đó bạn cố gắng truyền một số dữ liệu mà không đóng luồng. Nếu nhận được dữ liệu thì máy chủ có thể phản hồi thông qua một phương thức tìm nạp khác. Sau khi điều này xảy ra, bạn sẽ biết ứng dụng hỗ trợ 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 muốn biết, dưới đâ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 phần nội dung. Vì vậy, nếu trình duyệt không hỗ trợ luồng yêu cầu, thì phần 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 phần nội dung, chuỗi này sẽ thiết lập tiêu đề Content-Type thành text/plain;charset=UTF-8 một cách thuận tiện. Vì vậy, nếu bạn đặt tiêu đề đó, thì tức là 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 hỗ trợ luồng trong các đối tượng yêu cầu, nhưng không cho phép sử dụng các luồng này với fetch, vì vậy, tuỳ chọn duplex sẽ được kiểm tra mà Safari hiện không hỗ trợ.

Sử dụng với luồng có thể ghi

Đôi khi, việc sử dụng luồng sẽ dễ dàng hơn khi bạn có WritableStream. Bạn có thể thực hiện việc này bằng cách sử dụng luồng "identity". Đây là một cặp có thể đọc/có thể ghi, lấy mọi dữ liệu được truyền đến đầu có thể ghi và gửi đến đầu có thể đọc được. Bạn có thể tạo một trong những lớp này bằng cách tạo TransformStream mà không có 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 soạn các luồng cùng nhau. Dưới đây là một ví dụ ngớ ngẩn khi dữ liệu được tìm nạp từ một URL, sau đó được 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.