继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

走近Node.js Stream流式编程

scq000
关注TA
已关注
手记 22
粉丝 14
获赞 130

流(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中的流有各种的组合方式,流可以多路并行也可以合并,在方向上也提供了读入和写入两个方向。这就为各种数据处理,各种流式应用的开发提供了很好的技术基础。如果在开发过程中无法使用一个流进行处理,建议可以拆分任务,链式处理。

参考资料

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP