Chrome浏览器的开发者倡导者Jake Archibald将2016年称为 "网络流之年"。 显然,他的预测有些为时过早。流媒体标准早在2014年就已宣布。虽然花了一些时间,但现在在现代浏览器(仍在等待Firefox......)和Node(和Deno)中实现了一个一致的流媒体API。
什么是流?
流涉及到将资源分割成更小的块,称为块,并一次处理每个块。与其需要等待完成所有数据的下载,通过流,你可以在第一块数据可用时逐步处理数据。
有三种流:可读流、可写流和转换流。 可读流是数据块的来源。例如,底层数据源可以是一个文件或HTTP连接。然后,数据可以(有选择地)被一个转换流修改。然后,这些数据块可以被输送到一个可写流中。
随处可见的网络流
Node一直都有它自己的流的类型。它们通常被认为是很难操作的。网络超文本应用技术工作组(WHATWG)的网络流标准是后来才有的,而且在很大程度上被认为是一种改进。Node文档称它们为 "网络流",听起来不那么麻烦。最初的Node流并没有被废弃或删除,但它们现在将与Web标准流API共存。这使得编写跨平台的代码更加容易,也意味着开发者只需要学习一种做事的方式。
Deno是Node最初的创造者在服务器端JavaScript方面的另一次尝试,它一直与浏览器API紧密结合,并完全支持网络流。Cloudflare工作者(有点像服务工作者,但在CDN边缘位置运行)和Deno Deploy(Deno的无服务器产品)也支持流。
fetch() 响应作为一个可读流
有多种方法可以创建可读流,但调用fetch() ,这必然是最常见的。fetch() 的响应体是一个可读流:
fetch('data.txt')
.then(response => console.log(response.body));
如果你看一下控制台日志,你可以看到一个可读流有几个有用的方法。正如规范所说,一个可读流可以直接通过管道输送到一个可写流,使用其pipeTo() 方法,或者可以先通过一个或多个转换流,使用其pipeThrough() 方法。
与浏览器不同,Node core目前没有实现fetch。node-fetch,一个试图与浏览器标准的API相匹配的流行依赖,返回一个节点流,而不是一个WHATWG流。Undici,一个来自Node.js团队的改进的HTTP/1.1客户端,是Node.js核心的一个现代替代方案。 http.request(像node-fetch和Axios都是建立在这个基础之上的)。Undici已经实现了fetch ,而且response.body ,确实返回了一个网络流。🎉是同步的,所以你可以在这里找到你想要的信息。
Undici最终可能会出现在Node.js的核心中,而且它看起来会成为Node中处理HTTP请求的推荐方式。一旦你npm install undici并导入fetch ,它的工作原理就和在浏览器中一样。在下面的例子中,我们通过一个转换流来管理流。流中的每个块都是一个Uint8Array 。 Node核心提供了一个TextDecoderStream ,用于解码二进制数据:
import { fetch } from 'undici';
import { TextDecoderStream } from 'node:stream/web';
async function fetchStream() {
const response = await fetch('https://example.com')
const stream = response.body;
const textStream = stream.pipeThrough(new TextDecoderStream());
}
response.body 是同步的,所以你不需要 。在浏览器中, 和 是在全局对象上可用的,所以你不会包括任何导入语句。除此以外,代码对Node和Web浏览器来说是完全一样的。Deno还内置了对await fetch TextDecoderStream fetch和 TextDecoderStream.
异步迭代
for-await-of循环是for-of循环的一个异步版本。普通的for-of循环用于在数组和其他可迭代数据上循环。例如,for-await-of 循环可以用来在一个承诺数组上进行迭代:
const promiseArray = [Promise.resolve("thing 1"), Promise.resolve("thing 2")];
for await (const thing of promiseArray) { console.log(thing); }
对我们来说重要的是,这也可以用来迭代流:
async function fetchAndLogStream() {
const response = await fetch('https://example.com')
const stream = response.body;
const textStream = stream.pipeThrough(new TextDecoderStream());
for await (const chunk of textStream) {
console.log(chunk);
}
}
fetchAndLogStream();
流的异步迭代可以在Node和Deno中使用。所有的现代浏览器都有for-await-of循环,但它们还不能用于流。
获取可读流的一些其他方法
Blob 和File 都有一个.stream() 方法来返回一个可读的流。下面的代码可以在现代浏览器以及Node和Deno中使用--尽管在Node中,你需要在使用它之前import { Blob } from 'buffer';:
const blobStream = new Blob(['Lorem ipsum'], { type: 'text/plain' }).stream();
这里是一个基于浏览器的前端例子。如果你在你的标记中有一个<input type="file"> ,很容易得到用户选择的文件作为一个流:
const fileStream = document.querySelector('input').files[0].stream();
在Node 17中发货,由fs/promisesopen() 函数返回的FileHandle对象有一个.readableWebStream() 方法:
import {
open,
} from 'node:fs/promises';
const file = await open('./some/file/to/read');
for await (const chunk of file.readableWebStream())
console.log(chunk);
await file.close();
流与承诺很好地工作
如果你需要在流完成后做一些事情,你可以使用承诺:
someReadableStream
.pipeTo(someWritableStream)
.then(() => console.log("all data successfully written"))
.catch(error => console.error("something went wrong", error))
或者,你可以选择性地等待结果:
await someReadableStream.pipeTo(someWritableStream)
创建你自己的转换流
我们已经看到了TextDecoderStream (也有一个TextEncoderStream )。你也可以从头开始创建你自己的转换流。TransformStream 构造函数可以接受一个对象。你可以在这个对象中指定三个方法。start,transform 和flush 。它们都是可选的,但transform 是实际进行转换的。
作为一个例子,让我们假设TextDecoderStream() 不存在,并实现同样的功能(但一定要在生产中使用TextDecoderStream ,因为下面是一个过于简化的例子):
const decoder = new TextDecoder();
const decodeStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(decoder.decode(chunk, {stream: true}));
}
});
每个收到的块都被修改,然后由控制器转发。在上面的例子中,每个块是一些被编码的文本,被解码后转发。让我们快速看一下其他两种方法:
const transformStream = new TransformStream({
start(controller) {
// Called immediately when the TransformStream is created
},
flush(controller) {
// Called when chunks are no longer being forwarded to the transformer
}
});
转换流是一个可读流和一个可写流一起工作,通常用于转换一些数据。用new TransformStream() 制作的每个对象都有一个叫做readable 的属性,这是一个ReadableStream ,还有一个叫做writable 的属性,这是一个可写流。调用someReadableStream.pipeThrough() ,将数据从someReadableStream 写到transformStream.writable ,可能会对数据进行转换,然后将数据推送到transformStream.readable 。
有些人发现创建一个实际上并不转换数据的转换流是有帮助的。这就是所谓的 "身份转换流"--通过调用new TransformStream() ,不传递任何对象参数,或者不使用转换方法来创建。它将所有写到它的可写面的块转发到它的可读面,没有任何变化。作为这个概念的一个简单例子,"hello "是由以下代码记录的:
const {readable, writable} = new TransformStream();
writable.getWriter().write('hello');
readable.getReader().read().then(({value, done}) => console.log(value))
创建你自己的可读流
我们可以创建一个自定义的流,用你自己的块来填充它。new ReadableStream() 构造函数接收一个对象,该对象可以包含一个start 函数、一个pull 函数和一个cancel 函数。这个函数在创建ReadableStream 时被立即调用。在start 函数内部,使用controller.enqueue 来向流中添加块。
下面是一个基本的 "hello world "例子:
import { ReadableStream } from "node:stream/web";
const readable = new ReadableStream({
start(controller) {
controller.enqueue("hello");
controller.enqueue("world");
controller.close();
},
});
const allChunks = [];
for await (const chunk of readable) {
allChunks.push(chunk);
}
console.log(allChunks.join(" "));
下面是一个更真实的例子,取自流规范,将Web套接字变成可读流:
function makeReadableWebSocketStream(url, protocols) {
let websocket = new WebSocket(url, protocols);
websocket.binaryType = "arraybuffer";
return new ReadableStream({
start(controller) {
websocket.onmessage = event => controller.enqueue(event.data);
websocket.onclose = () => controller.close();
websocket.onerror = () => controller.error(new Error("The WebSocket errored"));
}
});
}
Node流的互操作性
在Node中,旧的针对Node的流工作方式并没有被移除。旧的节点流API和Web流API将共存。因此,有时可能需要使用.fromWeb() 和.toWeb() 方法将Node流变成Web流,反之亦然,这些方法将在Node 17中添加。
import {Readable} from 'node:stream';
import {fetch} from 'undici';
const response = await fetch(url);
const readableNodeStream = Readable.fromWeb(response.body);
结论
ES模块、EventTarget 、AbortController 、URL解析器、Web Crypto、Blob 、TextEncoder/Decoder :越来越多的浏览器API最终会出现在Node.js中。这些知识和技能是可以转移的。Fetch和流是这种融合的一个重要部分。
Domenic Denicola是streams规范的共同作者,他写道,streams API的目标是为I/O提供一个高效的抽象和统一的原语,就像承诺成为异步性的原语一样。为了在前端变得真正有用,更多的API需要实际支持流。目前,MediaStream,尽管它的名字,并不是一个可读的流。如果你正在处理视频或音频(至少目前是这样),一个可读流不能被分配到srcObject 。或者说,你想得到一张图片并通过一个转换流,然后把它插入到页面上。在写这篇文章的时候,使用流作为图像元素的src的代码有些冗长:
const response = await fetch('cute-cat.png');
const bodyStream = response.body;
const newResponse = new Response(bodyStream);
const blob = await newResponse.blob();
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;