理解 HTTP 响应和流

1. 理解响应对象

响应对象表示从请求返回的 HTTP 响应。

创建响应:

js 复制代码
const response = new Response("Hello world");

// 关键属性,告诉我们有关响应的信息:
console.log(response.status); // 200(默认值)
console.log(response.statusText); // OK
console.log(response.ok); // true(状态在 200-299 范围内)
console.log(response.headers); // Headers 对象

响应接口提供有关 HTTP 响应的信息:

  • status:HTTP 状态码(200、404、500 等)
  • ok:布尔值,指示状态是否在成功范围内(200-299)
  • headers:访问响应标头

一个重要的概念是,响应主体只能被消费一次:

js 复制代码
const response = new Response('{"message": "hello"}');

// 这可以正常工作
const data = await response.json();
console.log(data); // { message: 'hello' }

// 这会失败
try {
  const data2 = await response.json();
} catch (error) {
  console.log("错误:主体已被消费!");
}

如果你需要多次读取主体,在第一次消费之前使用 clone() 方法:

js 复制代码
const response = new Response('{"message": "hello"}');
const clone = response.clone(); // 在消费之前创建副本

const data1 = await response.json();
const data2 = await clone.json(); // 可以正常工作!使用的是副本

大多数时候,你会通过 fetch 获取响应对象:

js 复制代码
// Fetch 返回一个 Promise<Response>
const response = await fetch("https://api.example.com/data");

// 检查响应状态
if (!response.ok) {
  throw new Error(`HTTP 错误!状态:${response.status}`);
}

// 检查并处理内容类型
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
  const data = await response.json();
  // 处理你的 JSON 数据
}

如果已知是 JSON,则无需检查内容类型。

2. 消费响应的不同方式

我们已经见过 .json().clone() 的用法。

让我们探索所有消费方法:

js 复制代码
// .text() 用于纯文本数据
const textResponse = new Response("Hello, world");
const text = await textResponse.text();
console.log(text); // "Hello, world"

// .blob() 用于二进制数据,如图像
const imageResponse = await fetch("image.png");
const blob = await imageResponse.blob();
const imageUrl = URL.createObjectURL(blob);

// .arrayBuffer() 用于原始二进制数据
const buffer = await imageResponse.arrayBuffer();
const uint8Array = new Uint8Array(buffer);

// .formData() 用于表单数据
const formResponse = new Response("first_name=John&last_name=Doe", {
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
const formData = await formResponse.formData();
console.log(formData.get("first_name")); // 'John'

这些方法的关键点:

  • 每种方法返回一个 Promise,解析为相应的数据类型
  • 一旦使用了这些方法中的任何一个,响应主体就会被消费!
  • 根据你期望的数据类型选择方法:
    • .json() 用于 JSON 数据
    • .text() 用于纯文本、HTML、XML 等
    • .blob() 用于文件、图像
    • .arrayBuffer() 当你需要直接处理二进制数据时
    • .formData() 用于表单提交

什么是二进制数据?

以 1 和 0 组成的原始数据。计算机中的所有内容都是以不同的方式解释的二进制数据。

例如:字母 'A' 的二进制形式是 01000001(十进制中的 65)。

ASCII 是什么(美国信息交换标准代码)?

ASCII 是一个字符编码标准,其中每个字母/符号映射到一个数字(0-127)。

现代系统大多使用 UTF-8(在 0-127 范围内与 ASCII 兼容)

js 复制代码
const a = 97; // 小写字母 'a' 的 ASCII 码
const A = 65; // 大写字母 'A' 的 ASCII 码

const text = "hello";
// 将字符串转换为使用 UTF-8 编码的 Uint8Array
const asArray = new TextEncoder().encode(text);
console.log(asArray); // Uint8Array: [104, 101, 108, 108, 111]

什么是 ArrayBuffer?

ArrayBuffer 是原始二进制数据缓冲区,只是内存中的字节。

你不能直接操作 ArrayBuffer。它只是原始内存。你需要使用视图(一种读写 ArrayBuffer 的方式)来访问它。

js 复制代码
const buffer = new ArrayBuffer(4); // 4 字节的内存

const view = new Uint8Array(buffer);
view[0] = 104; // 'h'
view[1] = 105; // 'i'

什么是 Uint8Array?

Uint8Array 是一种处理 8 位无符号整数的类型化数组。

每个元素是 8 位(1 字节),允许的值范围是 0 到 255。

js 复制代码
const array = new Uint8Array([104, 101, 108, 108, 111]); // ASCII 中的 "hello"
console.log(array[0]); // 104('h' 的 ASCII 码)

// 常见的创建方式
const empty = new Uint8Array(5); // 创建一个包含 5 个零的数组
const fromArray = new Uint8Array([1, 2, 3]); // 从普通数组创建
const fromBuffer = new Uint8Array(someArrayBuffer); // 从 ArrayBuffer 创建

// 你不能存储超出 0-255 范围的值
const array2 = new Uint8Array([256, -1, 1.5]);
console.log(array2); // [0, 0, 1](值被转换)

通常用于:

  • 处理网络请求
  • 读写文件
  • 处理流
  • 处理图像或音频数据

它们适用于这些用途的原因是,它们是固定大小的 8 位无符号整数数组。为什么 8 位很好:

  • 与网络传输数据的方式相匹配(一次传输一个字节)
  • 计算机中最基本的数据单位是一个字节(8 位)

3. 流

我们已经看到,Response.body 是一个可读流。

js 复制代码
// 基本流读取
const response = await fetch("large-file.mp4");
const reader = response.body.getReader();

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

  // value 是一个 Uint8Array 块
  processChunk(value);
}

可读流表示一个你可以逐块读取数据的源。

  • 读取操作返回一个包含 {done, value} 的对象
  • value 通常是 Uint8Array 块
  • 当流结束时,done 变为 true

创建你自己的可读流

你可以创建自己的可读流:

js 复制代码
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("第一块");
    controller.enqueue("第二块");
    controller.close(); // 关闭流
  },
});

为什么需要流?

如果不存在流,你将不得不将整个文件加载到内存中。

js 复制代码
const response = await fetch("huge-file.mp4");
const blob = await response.blob(); // 内存:💥

// 使用流 - 逐块处理
const response = await fetch("huge-file.mp4");
for await (const chunk of response.body) {
  // 内存:✅ 每次只处理一块
  uploadChunk(chunk);
}

合并块

js 复制代码
// 将块收集到 Blob 中
const response = await fetch("some-file.mp4");
const chunks = [];

// 方法 1:使用 getReader
const reader = response.body.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(value);
}

// 这是最终的 Blob
// 可以直接使用或传递给文件输入
const blob = new Blob(chunks);

// 方法 2:使用 for await...of
// 更易读但概念相同
const chunks2 = [];
for await (const chunk of response.body) {
  chunks2.push(chunk);
}
const blob2 = new Blob(chunks2);

更多信息:

  • for await...of
  • getReader

转换流

转换流是一种可以在数据流经时修改数据的流。

js 复制代码
const transformStream = new TransformStream({
  transform(chunk, controller) {
    // 示例:如果块是文本,将其转换为大写
    const upperChunk = chunk.toString().toUpperCase();
    controller.enqueue(upperChunk);
  },
});

// 通过转换流管道
const response = await fetch("data.txt");
const transformedStream = response.body.pipeThrough(transformStream);

// 现在读取转换后的数据
for await (const chunk of transformedStream) {
  console.log(chunk); // 大写的块
}

什么为你处理好了?

  • 回压管理
  • 内部队列
  • 流锁定
  • 内存管理

最佳实践

1. 内存效率

js 复制代码
// 不好:加载整个文件
const response = await fetch("huge-video.mp4");
const blob = await response.blob(); // 内存:💥

// 好:逐块处理
const response = await fetch("huge-video.mp4");
for await (const chunk of response.body) {
  processVideoChunk(chunk); // 内存:✅
}

2. 取消操作很重要

如果你不在错误时取消读取器,可能会遇到以下问题:

js 复制代码
// 问题场景
const reader = response.body.getReader();

try {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    // 如果这里发生错误
    processChunk(value);
    // 读取器未被取消
  }
} catch (error) {
  // ❌ 没有 reader.cancel()
  throw error;
}

主要问题:

  • 资源泄漏:底层流资源保持打开状态
  • 内存泄漏:任何内部缓冲区保持分配状态
  • 网络连接:如果是网络流,连接可能会保持打开状态
  • 其他读取器无法访问:流保持锁定状态(其他读取器无法从中读取)

处理这种情况的良好模式:

js 复制代码
const reader = response.body.getReader();

try {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    processChunk(value);
  }
} catch (error) {
  reader.cancel();
  throw error;
} finally {
  // 最佳实践:在 finally 中取消以确保清理
  // 即使上面的代码没有抛出异常
  reader.cancel();
}

3. 常见的流管道

js 复制代码
// Fetch -> 转换 -> 处理 -> 保存
const response = await fetch("data.json");
const transformed = response.body
  .pipeThrough(parseJSON)
  .pipeThrough(filterData)
  .pipeThrough(compressData)
  .pipeTo(saveToFileStream);

实际用途回顾

  • 大文件处理(上传/下载)
  • 实时数据(视频/音频)
  • 逐步加载(滚动时加载)
  • 数据转换(压缩、加密)
  • 网络效率(不必等待所有数据加载)

原文:dev.to/abdullah-de...

相关推荐
涵信12 分钟前
第九节:React HooksReact 18+新特性-React 19的use钩子如何简化异步操作?
前端·javascript·react.js
Aaaaaaaaaaayou20 分钟前
浅玩一下 Mobile Use
前端·llm
这个昵称也不能用吗?21 分钟前
react-native搭建开发环境过程记录
前端·react native·cocoapods
hy_花花22 分钟前
Vue3.4之defineModel的用法
前端·vue.js
DataFunTalk36 分钟前
Foundation Agent:深度赋能AI4DATA
前端·后端·算法
hboot38 分钟前
rust 全栈应用框架dioxus
前端·rust·全栈
我是仙女你信不信43 分钟前
生成pdf并下载
前端·javascript·vue.js
少糖研究所43 分钟前
记一次Web Worker的使用
前端·性能优化
乔乔不姓乔呀1 小时前
pc 和大屏如何适配
前端
speedoooo1 小时前
新晋前端框架技术:小程序容器与SuperApp构建
前端·小程序·前端框架·web app