为 Web 开发者解释 Streams
当你调用 fetch() 并等待响应时,浏览器实际上已经在分块接收数据了。Web Streams API 让你的 JavaScript 代码能够在这些数据块到达时就访问它们,而不是等待整个响应完全加载后才能处理。
这种转变——从”等待所有数据”到”边到达边处理”——就是 streams 的核心所在。
核心要点
- Web Streams API 让你能够在数据到达时增量处理,而不是将整个响应缓冲到内存中。
ReadableStream、WritableStream和TransformStream是三个核心原语——可组合的数据管道构建块。fetch()返回的response.body是最常见的入口点:一个可以逐块读取的ReadableStream。- 使用
pipeThrough()和pipeTo()将转换和输出链接在一起,内置自动背压处理机制。
为什么一次性加载所有数据是个问题
传统的数据获取方式是这样的:
const response = await fetch('/large-dataset.json')
const data = await response.json()
// Nothing happens until all bytes are downloaded and parsed
对于小型数据,这没问题。但对于 50MB 的 JSON 文件或长时间运行的 API 响应,你需要在处理单条记录之前将整个数据保存在内存中。在资源受限的设备或慢速连接上,这意味着 UI 卡顿、内存压力大,以及用户体验糟糕。
Streams 让你能够在第一个数据块到达时就开始处理数据。
Web Streams API 的三个核心原语
Web Streams API 围绕三个类构建:
ReadableStream— 你从中读取数据的源WritableStream— 你向其写入数据的目标TransformStream— 位于中间,从一侧读取数据并将转换后的数据写入另一侧
数据以**块(chunks)**的形式在这些 streams 中流动——每次处理一小部分。一个 chunk 可以是字节的 Uint8Array、字符串或任何 JavaScript 值,具体取决于 stream 的类型。
Fetch 流式传输:增量读取响应
大多数 fetch() 响应通过 response.body 将其主体暴露为 ReadableStream。这是前端开发者进入 JavaScript streams 最常见的入口点。
async function processLargeResponse(url) {
const response = await fetch(url)
const reader = response.body.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
console.log(decoder.decode(value, { stream: true }))
}
} finally {
reader.releaseLock()
}
}
reader.read() 返回一个 promise,解析为 { value, done }。当 done 为 true 时,stream 结束。这种模式让你能够逐块处理数兆字节的响应,而无需缓冲整个内容。
关于流式请求体的说明: 将
ReadableStream作为fetch()请求体传递是可能的,但浏览器支持并不均衡。流式响应是目前支持良好、实用的模式。
Discover how at OpenReplay.com.
使用 pipeThrough() 和 pipeTo() 构建数据管道
Streams 真正强大之处在于组合能力。你可以将 ReadableStream 通过一个或多个 TransformStream 实例进行链式处理,并将结果导入 WritableStream。
fetch('./data.txt').then((response) =>
response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase())
}
}))
.pipeTo(new WritableStream({
write(chunk) {
document.body.textContent += chunk
}
}))
)
这个管道将字节解码为文本,将每个 chunk 转换为大写,然后写入 DOM——全程增量处理,无需等待完整响应。
pipeThrough() 将 ReadableStream 连接到 TransformStream 并返回一个新的 ReadableStream。pipeTo() 将 ReadableStream 连接到 WritableStream 并返回一个 promise,在 stream 完成时解析。
背压(Backpressure):Streams 如何避免过载
当消费者处理数据的速度慢于生产者生成数据的速度时,streams 会应用背压——一个信号向后传播通过管道链,告诉源头减速。当你使用 pipeTo() 和 pipeThrough() 时,这会自动发生。这是优先使用管道而非手动循环读取 chunks 的主要原因之一。
值得了解的内置 Streams
浏览器提供了几个现成的 stream 工具:
TextDecoderStream/TextEncoderStream— 在字节和字符串之间转换CompressionStream/DecompressionStream— 即时 gzip 或 deflate 压缩/解压数据Blob.stream()— 将任何Blob或File作为ReadableStream读取
现代 Node.js 也支持 Web Streams API,因此你为浏览器构建的管道可以无缝迁移到服务器端环境。
结论
Web Streams API 为前端开发者提供了一种可组合、内存高效的方式来处理随时间到达的数据。ReadableStream 和 TransformStream 是你最常使用的原语——特别是与 fetch() 结合进行增量响应处理时。从 response.body 开始,当需要转换数据时使用 pipeThrough(),让背压机制为你处理流量控制。
常见问题
可以。ReadableStream、WritableStream、TransformStream 以及管道方法在所有现代浏览器中都受支持,包括 Chrome、Firefox、Safari 和 Edge。通过 response.body 进行流式 fetch 响应体也得到广泛支持。使用 fetch 进行流式请求体的支持更有限,因此在依赖该功能之前请检查兼容性表。
如果管道链中的任何阶段抛出错误,错误会在管道中传播。可读端会变为错误状态,可写端会被中止。你可以通过向 pipeTo 传递带有 signal 的选项对象或捕获返回的 promise 来处理这种情况。对于手动读取循环,将 read 调用包装在 try-catch 块中。
Node.js 最初提供了自己的 stream API,包含 Readable、Writable 和 Transform 类。Web Streams API 是为浏览器设计的独立标准。现代版本的 Node.js 同时支持两者。Web Streams API 使用基于 promise 的拉取模型,而经典的 Node streams 使用基于事件的推送模型。基于 Web Streams API 编写的代码可以在浏览器和服务器环境之间移植。
如果响应很小,比如几百 KB 以下,使用 response.json 或 response.text 进行缓冲更简单且完全高效。Streams 在处理大型数据、实时数据或希望在完整响应到达之前显示部分结果的情况下才有价值。对于返回紧凑 JSON 的常规 API 调用,传统方法就足够了。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.