JavaScript Node.js流
什么是流(Stream)?
在Node.js中,流(Stream)是处理读写数据的一种方式,它允许我们以连续的方式处理数据,而不需要一次性将数据全部加载到内存中。这在处理大文件或网络通信时特别有用,因为它可以显著降低内存使用量,提高应用程序的性能和响应速度。
提示
想象一下,流就像是自来水管中的水流,数据像水一样从一端流向另一端,你可以在途中对这些数据进行处理。
为什么需要流?
假设我们需要读取一个1GB大小的文件并发送给客户端,有两种方式:
- 不 使用流:一次性读取整个文件到内存,然后发送给客户端
- 使用流:读取一小块数据,立即处理并发送,然后继续读取下一块
使用流的优势显而易见:
- 减少内存占用
- 提高数据处理速度
- 更好的用户体验(可以更快地看到部分结果)
Node.js中流的类型
Node.js中有四种基本类型的流:
- 可读流(Readable) - 用于读取数据(如文件读取)
- 可写流(Writable) - 用于写入数据(如文件写入)
- 双工流(Duplex) - 既可读又可写(如TCP连接)
- 转换流(Transform) - 一种特殊的双工流,可以在读写过程中修改或转换数据(如压缩/解压缩)
流的事件与方法
所有的流都是 EventEmitter 的实例,它们会发出可以监听和处理的事件。
常见的可读流事件
data
- 当有数据可读时触发end
- 当没有更多数据可读时触发error
- 发生错误时触发close
- 流关闭时触发
常见的可写流事件
drain
- 可以继续写入数据时触发finish
- 所有数据已被写入系统底层时触发error
- 写入或管道出错时触发close
- 流关闭时触发
使用流读取文件示例
下面是一个使用可读流来读取文件内容的简单示例:
const fs = require('fs');
// 创建可读流
const readStream = fs.createReadStream('example.txt', { encoding: 'utf8' });
// 监听数据事件
readStream.on('data', (chunk) => {
console.log('收到数据片段:');
console.log(chunk);
});
// 监听结束事件
readStream.on('end', () => {
console.log('文件读取完毕');
});
// 监听错误事件
readStream.on('error', (err) => {
console.error('发生错误:', err);
});
输出示例:
收到数据片段:
这是文件的第一部分内容...
收到数据片段:
这是文件的第二部分内容...
文件读取完毕
使用流写入文件示例
以下是使用可写流将数据写入文件的示例:
const fs = require('fs');
// 创建可写流
const writeStream = fs.createWriteStream('output.txt');
// 写入数据
writeStream.write('Hello, ');
writeStream.write('Node.js ');
writeStream.write('Streams!\n');
// 标记写入结束
writeStream.end();
// 监听完成事件
writeStream.on('finish', () => {
console.log('写入完成');
});
// 监听错误事件
writeStream.on('error', (err) => {
console.error('写入错误:', err);
});
输出:
写入完成
生成的output.txt文件内容:
Hello, Node.js Streams!
流的管道(Pipe)
流的强大之处在于可以使用管道(pipe)将它们链接起来。使用 pipe()
方法可以将一个可读流的输出直接连接到一个可写流的输入:
const fs = require('fs');
// 创建可读流和可写流
const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('destination.txt');
// 使用管道连接两个流
readStream.pipe(writeStream);
// 当管道完成时输出消息
writeStream.on('finish', () => {
console.log('文件复制完成');
});
备注
pipe()
方法自动处理背压(backpressure)问题,确保目标写入流不会被数据淹没,这是手动处理流时需要特别注意的问题。
创建自定义流
在某些情况下,你可能需要创建自定义的流来满足特定需求。以下是一个创建自定义转换流的示例,它将输入的文本转换为大写:
const { Transform } = require('stream');
// 创建自定义转换流
class UppercaseTransform extends Transform {
_transform(chunk, encoding, callback) {
// 将数据块转换为大写并推送到输出队列
this.push(chunk.toString().toUpperCase());
callback();
}
}
// 使用自定义转换流
const uppercaseTransform = new UppercaseTransform();
// 从标准输入读取,通过转换流处理,然后写入标准输出
process.stdin
.pipe(uppercaseTransform)
.pipe(process.stdout);
console.log('请输入一些文本:');
运行以上代码,当你在控制台输入 "hello world" 时,它会输出 "HELLO WORLD"。
实际应用场景
1. 文件压缩
使用流来压缩文件,无需一次性读取整个文件:
const fs = require('fs');
const zlib = require('zlib');
// 创建可读流、gzip转换流和可写流
const readStream = fs.createReadStream('large-file.txt');
const gzip = zlib.createGzip();
const writeStream = fs.createWriteStream('large-file.txt.gz');
// 使用管道链接流
readStream
.pipe(gzip)
.pipe(writeStream);
writeStream.on('finish', () => {
console.log('文件压缩完成');
});
2. HTTP响应流
在Web服务器中使用流来发送大文件:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
// 设置内容类型
res.setHeader('Content-Type', 'video/mp4');
// 创建视频文件的可读流
const videoStream = fs.createReadStream('large-video.mp4');
// 直接将文件流通过管道连接到HTTP响应
videoStream.pipe(res);
// 处理可能的错误
videoStream.on('error', (err) => {
console.error('流错误:', err);
res.statusCode = 500;
res.end('Server Error');
});
});
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});