流(Stream)应该算是Node.js中最重要的概念之一。在通常的编程范式中,都是从输入端接收到所有数据后,缓存到内存中,然后最后一次性处理。而使用流机制则可以边接收数据边处理,这也使得我们能够更加高效地进行实时的IO操作。接下来这篇文章,将主要介绍Node.js中流的一些基本概念以及常用的接口用法。
到底流是什么?
在Node.js中,流(stream)是一种处理流式数据的抽象接口,我们平常所使用的文件、图片等二进制数据都可以通过流的方式在互联网上传输。通过这一抽象接口,可以对数据的内存底层实现方式进行封装,在实际开发过程中,假如你要开发流式应用,比较流行的实现方式主要有RxJS和Node.js中提供的Stream API。
Node.js中流根据用途,可以分为四类,其中比较常用的有可读流(Readable Stream)和可写流(Writable Stream)。它们在方向上是相互对应的。
下面是一些比较常用的可读流,可写流的例子:
可读流 | 写入流 |
---|---|
HTTP responses (客户端) | HTTP request (客户端) |
HTTP requests(服务器端) | HTTP response(服务端) |
fs read | fs write |
zlib | zlib |
crypto | crypto |
TCP sockets | TCP sockets |
process.stdin | process.stdout, process.stderr |
child process stdout, stderr | child process stdin |
要新建一个可读流,可以使用下面的代码:
const { Readable } = require("stream");
const readable = new Readable({
read(size) {
// 利用给定的大小读取数据
this.push(...)
}
})
与之相对应,如果要实现一个可写流,则要采用Writable
接口:
const { Writable } = require("stream");
const writable = new Writable({
write(chunk, encoding, callback) {
callback();
}
})
第三种是同时支持双向传输的流——duplex 流,实现上就是同时结合了可读流和可写流:
const { Duplex } = require("stream");
const duplex = new Duplex({
read(size) {
this.push(...)
}
write(chunk, encoding, callback) {
console.log(chunk.toString())
callback();
}
})
最后一种是传输流(Tranform Stream),它其实是可以在读写过程中修改和变换数据的Duplex流,在Node.js中内置的zlib.createDeflate
方法使用的就是这种流。
const { Transform } = require('stream');
const toUpperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
})
流的事件
在Node.js中,分别针对流数据传输过程中的一系列步骤都提供了相应接口。
在可读流中,主要有:
- close 关闭事件
- data 数据传输事件
- end 结束传输事件
- error 错误事件
- readable 事件
而可写流中,提供的事件类型则相对多一些:
- close 关闭事件
- drain
- finish 结束传输事件
- error 错误事件
- pipe 管道事件
- unpipe 移除管道事件
Node.js中流的行为逻辑都是基于事件监听的,所以,如果要处理数据,则要写好对应的监听代码。如:
const readStream = fs.createReadStream("demo.txt");
readStream.on('open', () => {
console.log('File has been opened');
});
readStream.on('data', data => {
console.log(data);
});
readStream.on('end', () => {
console.log('Reading file finished')
});
readStream.on('error', () => {
console.log('There is something errored');
});
一个流有两种状态,一种是flowing(流动状态),而另一种则是paused(暂停状态)。一般在读取文件过程中,文件打开后,流就进入了flowing状态,不过我们可以显式调用pause()
方法让其暂停文件的读取,直到再次调用resume
恢复其状态。
const readStream = fs.createReadStream("demo.txt");
readStream.on('open', () => {
console.log('File has been opened');
});
readStream.on('data', data => {
console.log(data);
readStream.pause();
setTimeout(() => {
console.log('resume');
readStream.resume();
}, 100);
});
readStream.on('end', () => {
console.log('Reading file finished')
});
readStream.on('error', () => {
console.log('There is something errored');
});
Object Mode
在默认情况下,写入流只支持字符串,Buffer和Uint8Array
等几种类型的数据,而如果要支持其他的JavaScript值,可以使用Object Mode
模式:
const objectStream = new Writable({
objectMode: true,
write(chunk, encoding, callback) {
log(chunk);
callback();
}
});
链式调用
要对流进行链式地管道处理,可以使用pipe
接口:
const filePath = path.join(__dirname, 'demo.txt');
const readStream = fs.createReadStream(filePath);
readStream
.pipe(parse)
.pipe(zip)
.pipe(process.stdout)
这可以类比于Linux系统中命令行的管道机制:
echo Hello World | sed s/He/s/g
平常开发API的时候,可能需要临时mock数据,然后直接返回,就可以使用文件流的方式进行:
const filePath = path.join(__dirname, 'mock.json');
app.get("/", (req, res) => {
fs.readFile(filePath, (err, file) => {
res.send(file)
})
})
上面这种写法是比较常规的写法,换一种链式调用的写法,可以像这样:
const filePath = path.join(__dirname, 'mock.json');
app.get("/", (req, res) => {
fs.createReadStream(filePath).pipe(res);
});
使用链式的写法,代码看上去会更加简洁,而且会更加符合职责单一的原则,将每一个数据处理都拆分成小步骤,依次执行。
总结
Node.js中的流有各种的组合方式,流可以多路并行也可以合并,在方向上也提供了读入和写入两个方向。这就为各种数据处理,各种流式应用的开发提供了很好的技术基础。如果在开发过程中无法使用一个流进行处理,建议可以拆分任务,链式处理。