Title image of Streams API by Mozilla Contributors is licensed under CC-BY-SA 2.5.
此文章的略微润色版可前往 知乎、掘金、SegmentFault、专栏 查看
在服务端开发中,流是一个很常见的概念。有了流我们就不再需要等待整个数据获取完毕后才处理数据,而是可以一段一段地拿到数据,在获得数据的同时直接解析数据。这样既可以高效利用 CPU 等资源,还减少了存放整个数据的内存占用。不过在过去,客户端 JavaScript 上都没有流的概念,而随着 Streams API 在各大浏览器上的逐步实现,我们终于可以使用原生的 API 以流的角度来看待数据了,例如从 fetch 请求上可以得到一个网络流。
既然标题是「从 Fetch 到 Streams」,那么首先让我们来看看既熟悉又陌生的 Fetch API。相比较于 XMLHttpRequest
,fetch()
的写法简单又直观,只要在发起请求时直接将整个配置项传入就可以了。fetch()
方法接受一个 Request
实例,或者是大家更常使用的方法——传入需要请求的 URL 以及一个可选的初始化配置项对象,然后就可以从 Promise
中取得返回的数据:
|
fetch('https://example.org/foo', { method: 'POST', mode: 'cors', headers: { 'content-type': 'application/json' }, credentials: 'include', redirect: 'follow', body: JSON.stringify({ foo: 'bar' }) }).then(res => res.json()).then(...) |
如果你不喜欢 Promise 的链式调用的话,还可以用 async
/await
:
|
const res = await fetch('https://example.org/foo', { ... }); const data = await req.json(); |
看起来相当的直观,在请求时直接将所有的参数传入 fetch()
方法即可,甚至相较于 XHR 还提供了更多的控制参数,例如是否携带 Cookie、是否需要手动跳转等;取出数据也只需要调用 Response
对象上的方法就能拿到格式化的数据(例如 res.json()
)。而直接使用 XMLHttpRequest
看起来就没那么方便了,初始化时既有方法的调用又有参数的赋值,看着还挺混乱的:
|
const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://example.org/foo'); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.responseType = 'json'; xhr.withCredentials = true; xhr.addEventListener('load', () => { const data = xhr.response; // ... }); xhr.send(JSON.stringify({ foo: 'bar' })) |
而随着 AbortController
与 AbortSignal
在各大浏览器上完整实现,Fetch API 也能像 XHR 那样中断一个请求了,只是稍微绕了一点。通过创建一个 AbortController
实例,我们就得到了一个可以控制中断的控制器。这个实例的 signal
参数提供了一个 AbortSignal
实例,还提供了一个 abort()
方法用于发送中断信号。我们将 signal
传递进 fetch()
的初始化参数中,就可以在 fetch 请求之外控制请求的中断了:
|
const controller = new AbortController(); const { signal } = controller; fetch('/foo', { signal }).then(...); signal.onabort = () => { ... }; controller.abort(); |
只可惜提出这个解决方案并实装的时间还是有点晚,Fetch API 早在 就在 Firefox 上实现,并且最早于 在 Chrome 上实装。而该功能最早也是 Edge 浏览器在 2017 年 4 月实现,Safari 直到 2018 年末才被发现 AbortController
不能中断请求的 bug,最后在 2019 年 3 月的 Safari 12.1 中才正式解决。算下来从 Fetch API 在浏览器上实装开始,到主流现代浏览器全部支持,跨越了整整四年。
Image of AbortController & AbortSignal Support Table by caniuse.com is licensed under CC-BY 4.0.
晚归晚,但是看起来现在的 Fetch API 已经无所不能了,不过在「真香」之前,我们来考虑一个很常见的场景:浏览器需要异步请求一个比较大的文件,由于可能比较耗时,希望在下载文件时展示文件的下载进度。XHR 提供了很多的事件,其中就包括了传输进度的 onprogress
事件,所以使用 XHR 可以很方便地实现这个功能:
|
const xhr = new XMLHttpRequest(); xhr.open('GET', '/foo'); xhr.addEventListener('progress', (event) => { const { lengthComputable, loaded, total } = event; if (lengthComputable) { console.log(`Downloaded ${loaded} of ${total} (${(loaded / total * 100).toFixed(2)}%)`); } else { console.log(`Downloaded ${loaded}`); } }); xhr.send(); |
但是 Fetch API 呢?你打开了 MDN,仔细地看了 fetch()
方法的所有参数,都没有找到类似 progress
这样的参数,毕竟 Fetch API 并没有什么回调事件。难道 Fetch API 就不能实现这么简单的功能吗?这里就要提到和它相关的 Streams API 了——不是 Web Socket,也不是 Media Stream,更不是只能在 Node.js 上使用的 Stream,不过和它很像。
READ MORE →