Node.js操作按需数据使用sream API接口,stream 是一个数据集,数据可能不能马上全部获取到,他们在缓冲区,不需要在内存中。适合处理大数据集或者来自外部的数据源的数据块 Node中很多内建模块实现了流式接口:
上面的列表中的原生Node.js对象就是可读流和可写流的对象。有些对象是可读流也是可写流,如TCP sockets,zlib 和 crypto streams
这些对象是密切相关。当一个HTTP响应在客户端上是一个可读流,相应的在服务端是一个可写流。这是因为在HTTP的情况下,我们基于从一对象(http.IncomingMessage)读而从另外一个对象(http.ServerResponse)写
stdio流(stdin,stdout,stderr)在子进程中会有反向流类型。这样的话就能用非常简单的方式管道传送给其他流或者主进程的stdio流。
Node.js中有4个基本的流类型:
- 可读流(Readable)
- 可写流(Writable)
- 双工流(Duplex)
- 转换流(Transform streams)
- 可读流是可以被消耗的数据源的抽象,典型例子就是fs.createReadStream方法。
- 可写流是可以写入数据的目的地的抽象,典型例子就是 fs.createWriteStream 方法。
- 双工流既是可读的也是可写的,典型例子是TCP套接字。
- 转换流是基于双工流的,它可以用来修改或转换数据,因为它是写入和读取的。 zlib.createGzip 就是一个用gzip来压缩数据的转换流例子。你可以认为转换流就是一个函数,这个函数的输入是一个可写流,输出是一个可读流,你可能也听说过把转换流叫做" 通过流 "。
所有的流都是 EventEmitter 的实例。他们在数据可读或者可写的时候发出事件。然而,我们也可以简单的通过 pipe 方法来使用流数据。
pipe方法:
**readable**.pipe(**writableDest**)
这简单的一行,连接了可读流的输出——源数据和可写流的输入——目标。源必须是可读流,目标必须是可写流。当然也可以是双工流或者转换流,事实上,如果连接的是一个双工流,可以链式调用pipe:
readable
.pipe(transformStream1)
.pipe(transformStream2)
.pipe(finalWrtitableDest)
pipe方法返回目标流,这使我们能够执行上面的链式调用。对于流a(可读)、b和c(双工)和d(可写)
a.pipe(b).pipe(c).pipe(d)
上面等价于
a.pipe(b)
b.pipe(c)
c.pipe(d)
pipe 方法是最简单的方式去使用流,一般建议使用 pipe 方法或使用事件来处理流,但是要避免两个混合使用。通常,当你使用 pipe 方法时,你不需要使用事件,但是如果你需要用更多定制的方式来处理流,那你可以只用事件。
流事件
除了从可读流里读取数据和向可写流目标写数据外,pipe方法将自动管理沿途的一些事情。例如,它处理错误、文件结束以及当一个流比另一个流慢或更快时的情况。
然而,我们也可以直接使用事件来操作流。下面是pipe方法主要用于读取和写入数据的事件的简化等效代码:
# readable.pipe(writable) 等于下面
readable.on('data', (chunk) => {
writable.write(chunk);
});
readable.on('end', () => {
writable.end();
});
以下是可读流可写流的重要事件以及可用方法:
这些事件和函数在某种程度上是相关的,因为它们通常一起使用。
可读流中最重要的事件是:
data
事件,每当流将数据块传递给消费者时,它就会触发。end
事件,当没有更多的数据从流中被消耗时触发。
可写流中最重要的事件是:
drain
事件,这是可写流可以接收更多数据的信号。finish
事件,当所有数据都给到底层系统时触发。
可以结合事件和函数来定制和优化流的使用。使用一个可读的流,我们可以用pipe
/ unpipe
方法,或read
/ Unshift
/ resume方法。使用一个可写流,我们可以把它pipe
/ unpipe
目的地,或是写它的write
方法调用end
方法当我们完成。
可读流的暂停和流(flowing)模式
可读流有两种主要模式,这影响我们可以使用它们的方式:
- 它们可以是
暂停(paused)
模式 - 或是
流(flowing)
模式
这些模式有时被称为拉和推模式。
所有可读的流默认情况下都是在暂停模式下启动的,但在需要时可以轻松切换到流模式或者返回到暂停状态。有时,转换是自动发生。
当一个可读流处于暂停模式,我们可以使用 read() 方法按需的从流中读取数据,然而,在流模式下的可读流,数据是不断流动的,我们要监听事件来使用这些数据。
在流模式下,如果没有用户处理数据,那么实际上数据会丢失。这就是为什么当我们在流模式中有可读的流时,我们需要一个 data 事件。事实上,只要添加一个 data 事件,就可以将暂停模式转换为流模式,删除 data 事件,流将切换回暂停模式。其中一些这样做事为了与旧的节点流接口向后兼容。
这两个流模式之间手动开关,可以使用 resume() 和 pause() 方法。
当使用 pipe 方法读取可读流时,我们不必担心这些模式,因为pipe自动管理它们。
实现流
当我们谈论Node.js中的流,主要有两种不同的任务:
- 实现流。
- 使用流。
流的实现通常会 引入 (require)stream
模块。
实现可写流
为了实现可写流,我们需要使用流模块中的 Writable 构造函数。
const { Writable } = require('stream');
我们有很多方式来实现一个可写流。例如,如果我们想要的话,我们可以继承Writable构造函数。
class myWritableStream extends Writable {
}
这里用简单的构造函数的方法。我们只需给 Writable 构造函数传递一些选项并创建一个对象。唯一需要的选项是 Writable 函数,该函数揭露数据块要往哪里写。
const { Writable } = require('stream');
const outStream = new Writable({
**write**(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
}
});
process.stdin.pipe(outStream);
这个write函数有3个参数:
- chunk 通常是一个buffer,除非我们配置不同的流。
- encoding 是在特定情况下需要的参数,通常我们可以忽略它。
- callback 是在完成处理数据块后需要调用的函数。这是写数据成功与否的标志。若要发出故障信号,请用错误对象调用回调函数。
在 outstream,我们只是用 console.log 把数据块作为一个字符串打印到控制台,然后不用错误对象调用 callback 表示成功。这是一个非常简单的可能也不那么有用的 echo 流,它把收到的所有数据打印到控制台。
为了使用这个流,我们可以直接用 process.stdin 这个可读流,就可以把 process.stdin pipe给 outStream.
执行上面的代码,任何我们输入给 process.stdin 的内容都会被 outStream 的 console.log 输出到控制台。
实现这个流不怎么有用,因为它实际上被实现了而且node内置了,它等同于 process.stdout。以下一行代码,就是把 stdin pipe给 stdout ,就能实现之前的效果:
process.stdin.pipe(process.stdout);
实现可读流
为了实现可读流,引用Readable接口并用它构造新对象:
const { Readable } = require('stream');
const inStream = new Readable({});
有一个简单的方法来实现可读流。我们可以直接把供使用的数据 push 出去。
const { Readable } = require('stream');
const inStream = new Readable();
inStream.push('ABCDEFGHIJKLM');
inStream.push('NOPQRSTUVWXYZ');
inStream.push(null); // No more data
inStream.pipe(process.stdout);
当 push 一个 null 对象就意味着我们想发出信号——这个流没有更多数据了。
使用这个可写流,可以直接把它pipe给 process.stdout 这个可写流。
执行以上代码,会读取 inStream 中所有的数据,并输出在标准输出流。很简单,也不是很有用。
我们基本上在pipe给 process.stdout 之前把所有的数据都推到流里了。更好的方法是按需推送。我们可以通过在一个可读流的配置实现 read() 方法来做这件事情:
const inStream = new Readable({
**read**(size) {
// there is a demand on the data... Someone wants to read it.
}
});
当在可读的流上调用读方法时,实现可以将部分数据推到队列中。例如,我们可以一次推送一个字母,从字符代码65(代表A),并且每推一次增加1:
const inStream = new Readable({
read(size) {
**this.push**(String.fromCharCode(this.currentCharCode++));
if (this.currentCharCode > 90) {
**this.push**(null);
}
}
});
inStream.currentCharCode = 65;
inStream.pipe(process.stdout);
当从可读流里读数据, read 方法将被持续调用,我们就会推送更多的字母。我们需要停止这个循环的条件,这就是为什么会一个if语句当currentcharcode大于90(代表Z)是推送null。
这段代码相当于我们开始使用的更简单的代码,但是当用户要求时,我们正在按需推送数据。你应该经常这样做。