
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);
实际用途回顾
- 大文件处理(上传/下载)
- 实时数据(视频/音频)
- 逐步加载(滚动时加载)
- 数据转换(压缩、加密)
- 网络效率(不必等待所有数据加载)