深入理解fetch api(2) --从.json()到流
1. 前言
自从开启这条支线,已经过去大半年了。回想当初,自己连 flex 都整不明白,硬是靠着“父相子绝”的糊弄完试用期大作业——现在总算算是初级前端开发者了。虽然还没自信能把前端请求的行业导向讲得面面俱到,但最近一篇文章似乎有一点点醒了我:
这篇文章提到 Node.js 和 Web Streams 的协作时,我突然意识到:其实 fetch 的默认返回,就是一个“流”!你看我在系列第一篇里展示过的 fetch response 对象:
1 |
|
这里的 ReadableStream,就是流(Stream)的一种。
所以,这一篇就打算顺着这个发现,和你一起聊聊流式数据处理,看看 fetch 背后的“流世界”到底有多精彩。
2. 流的简介
2.1 引入
在实际项目中,很多同学常用的都是 axios,所以对“流”这个概念可能有些陌生。
这是因为——
浏览器端的 axios 封装的是 XMLHttpRequest(XHR),而 Node 端走的是 http/https 模块,本质都是传统的“请求-响应”模型
也就是说:
axios 默认会把响应内容“全部接收完”,再一次性返回给你。
举个例子,你写了
1 |
|
拿到的 data
字段,就是服务器全部响应内容,不是一段段流出来的。
这里就有一个很现实的痛点:
假设你需要渲染一个 1GB 的 4K 超清长视频,
——等整个视频都下载完,用户可能都已经睡着了。😪
这正是“流”大显身手的地方:
利用流,我们可以让大文件像水管里的水一样“边下边用”。比如大视频能一边下载一边渲染,实现“秒开”、“不卡顿”的体验。
2.2 流的定义
刚才我们用简单的例子聊了什么是“流”,相信你已经有了初步的认识。不过,作为技术人,八股还是得补一点的
我们来看一下 WHATWG(Web 标准制定组织)对流(stream)的官方定义:
A stream represents a sequence of data made available over time.
直译过来就是:
流表示随着时间推移而可用的一系列数据。
这里有两个关键点你一定要记住——
“时间推移” 和 “一系列数据”。
等你去面试的时候,如果背出了这句话,你会感谢现在的自己!
2.3 流的分类
就像文件 IO(输入输出)有读和写的区分,网络 IO 也有自己的“门派划分”,流处理同样如此。
在 Web 和 Node.js 语境下,流大致可以分为三类:
- Readable Stream(可读流):只能读,比如从服务器下载文件、fetch 响应体;
- Writable Stream(可写流):只能写,比如上传文件到服务器;
- Transform Stream(转换流):边读边处理边写,比如实时解压、加密、格式转码等。
是不是突然觉得很熟悉?和我们日常开发里碰到的文件、网络操作非常类似。
不过,今天这篇文章我们就聚焦在最常用、最有“前端感”的那一类——**Readable Stream
**。接下来,就让我们一起看看怎么优雅地处理这样一根源源不断的数据水管
3. 流处理的简单实践
3.1 自定义流 模拟fetch返回
考虑到我的博客面向的大多数都是和我一样的前端开发者,现实里一时间很难找到一个“流式后端接口”练手,所以我们不如自己手搓一根“水管”,来模拟下真实 fetch 返回流的场景。
1 |
|
这里我们用 ReadableStream
构造函数新建了一个只读流对象,并注册了 start
回调,从回调参数里拿到了 controller
这个“调度员”。
controller 作为流的“指挥棒”,最常用的操作有这三种(更高级的像背压先不展开):
- enqueue(chunk): 往流里“推”一段数据
- close(): 关闭流
- error(e):中止流并抛出错误
由于流的本质是二进制传输,所以我们得用 TextEncoder
把字符串编码成 utf-8 的二进制数据,才能推到流里。
说到这里,这根“自制水管”应该就很好理解了:它就是按时间间隔,分三次往外流出三段数据,最后关闸。
3.2 消费流
假设现在这个流就是 fetch 的返回对象,我们要“接上水管”,首先需要用 getReader()
方法拿到“读取器”:
1 |
|
可以把这根水管想象成一个异步队列——数据是“先进先出”的。
reader
里有个 read()
方法,每次调用就能从队首读到一段数据。每次 read()
返回的结构长这样:
1 |
|
- done 表示流是否已经结束;
- value 是本次读到的流数据(二进制)。
因此基础结构就这么出来了
1 |
|
还记得我们要做什么吗?就是要把这根水管里的每一段数据不断输出。
但别忘了,流里传递的是二进制数据(Uint8Array
),所以我们要用 TextDecoder
把它解码成字符串:
1 |
|
这样,我们就实现了一个最基础的流式消费
3.3 完整代码
1 |
|
直接在node.js中跑就可以看到效果了
4. fetch中的流处理
前面我们已经用自定义流体验了一把“水管流数据”的快感,这里就让我们回到真实的 fetch,看看怎么利用流式 API 优雅地消费网络响应。
传统用法(对比):
1 |
|
- 缺点:必须等服务器把所有数据传完,才能用,遇到大文件/实时场景用户体验很差。
流式用法:
1 |
|
- 优点:每来一段数据就可以处理/渲染,体验拉满,特别适合大文件、流媒体、实时数据。
配合主流框架的响应式系统,就能实现边下边渲染,带来“秒开”的极致体验!
特别注意
ps. 需要注意的是并不是所有服务器或 CDN 都支持流式(chunked)传输。有些静态资源服务器/CDN 会在后台把整个文件读完后再一次性返回,
这样即使你用 response.body.getReader()
,实际上也是一次性拿到全部数据,无法真正实现“边下边用”的效果。
如何判断资源支持流式?
你可以在开发者工具的“网络(Network)”面板,查看响应头(Response Headers)——
- 如果你看到
Transfer-Encoding: chunked
说明服务器是分块传输,支持流式; - 如果响应头里没有
chunked
,而是有Content-Length: xxx
,往往意味着是“一次性传输”,不支持流式读取。
5. 结语
这些只是fetch流式传输的冰山一角,实际开发过程中,特别是涉及音视频处理的请求,复杂程度、坑点都超乎了我们的想象
不过不用紧张,这只是这个这个系列的第二篇,笔者的暑假也才刚刚开始
本文由chatgpt辅助生成