理解 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...

相关推荐
mldong10 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排10 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606111 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角12 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!12 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰12 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi14 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen15 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马15 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒16 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端