@zhangning16
2018-01-26T03:07:07.000000Z
字数 5061
阅读 298
node
文件的拷贝是一个很常见的需求,可是node的fs模块没有提供copy方法,但利用node提供的其他api可以很简单的实现一个copy方法。比如:
const fs = require('fs')
const file = fs.readFileSync('./init.txt', {encoding: 'utf8'})
fs.writeFileSync('./init_copy.txt', file)
这是用同步的方式先读取文件,拿到读取到数据后,再写入进一个新的文件,也就实现了一个复制功能。
但是这样写是问题,如果如果文件过大,比如读取一个1个G视频文件,等到读取完毕再进行写入操作,将占用系统非常大的内存,而且接收端的等待时间也较长,显然是不合理的。这时候就需要引入流的概念,一边流出,一边流入。
先来看看node.js官方文档的解释:
流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。
Node.js 提供了多种流对象。 例如, HTTP 请求 和 process.stdout 就都是流的实例。
流可以是可读的、可写的,或是可读写的。所有的流都是 EventEmitter 的实例。
Node.js 中有四种基本的流类型:
Readable - 可读的流 (例如 fs.createReadStream()).
Writable - 可写的流 (例如 fs.createWriteStream()).
Duplex - 可读写的流 (例如 net.Socket).
Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()).
这篇笔记只涉及到Readable和Writable,流强调的一边流入,一边流出,也就是一个产生数据,一个消费数据。把文件a复制到文件b,那么a就是提供可读的流,b就提供可写的流。
可读流是对数据源的一种抽象,是数据生产商。可读流具体又分为两种状态,流动状态(flowing)和暂停状态(paused)。这两种状态是可以互相转换的,当可读流在初次创建的时候默认是处于暂停态的。我们需要手动创建提供消费数据的机制,可读流才会主动提供数据。如果消费数据的机制不存在的话,生产数据将会变得没有意义,这时候可读流也就主动停止生产数据。
从暂停状态到流动状态有三种方法:
1. 给可读流添加‘data’事件监听函数。
2. 调用 writeStream.resume() 方法。
3. 调用 writeStream.pipe() 方法将数据发送到 Writable。
从流动状态到暂停状态有两种方法:
1. 如果不存在管道目标(pipe destination),可以通过调用 writeStream.pause() 方法实现。
2. 如果存在管道目标,可以通过取消 'data' 事件监听,并调用 writeStream.unpipe() 方法移除所有管道目标来实现。
// 引入node内置的fs文件处理模块
const fs = require('fs');
// 读取当前目录下的init.txt文件,并且返回的是一个可读流,用readStream代表这个可读流
const readStream = fs.createReadStream('./init.txt');
// 创建一个的init_copy.txt文件,并且返回的是一个可写流,用writeStream代表这个可写流
const writeStream = fs.createWriteStream('./init_copy.txt');
// 当系统缓存中有可读流有数据流出时,会不断的触发data事件
readStream.on('data', (chunk) => {
// 回调函数的第一个参数,是读取到的系统缓存的数据,可写流拿到这个数据,进行写操作
writeStream.write(chunk);
});
// 当可读流不再进行有数据流出后,将触发end事件,通过监听这个事件,手动关闭写操作。
readStream.on('end', (tunck) => {
console.log('主动关闭写入流');
writeStream.end();
})
这时候我们拷贝大文件的时候,就合理了许多,每次读一点,写一点,然后根据,不会有大量的文件堆在系统缓存中,但是仔细想想也有问题。比如这部分代码:
// 当系统缓存中有可读流有数据流出时,会不断的触发data事件
readStream.on('data', (chunk) => {
// 回调函数的第一个参数,是读取到的系统缓存的数据,可写流拿到这个数据,进行写操作
writeStream.write(chunk);
});
每当可读流输出数据时,都会触发‘data’事件,这段代码都会执行,但是并不能保证读取数据和写入数据的速度是完全一样的,如果可读流提供的数据的速度远远超过可写流写入的速度,那么很有可能数据读取不及时,导致数据的丢失。所以这段代码还需要继续优化。
可读流有两种状态,流动状态(flowing)和暂停状态(paused)。为什么需要这两个状态,回想一下上面的例子,如果我们可以等到每次写入的时候,判断是否写入完毕,如果写入完毕,就继续流入数据,如果没有写入完毕,就暂停流入数据,这样就可以避免由于写入不及时导致的数据丢失,类似开关水龙头。
那么我们对代码优化一下:
const fs = require('fs');
const readStream = fs.createReadStream('./init.txt');
const writeStream = fs.createWriteStream('./init_copy.txt');
readStream.on('data', (chunk) => {
// writeStream.write()方法被调用后,会返回一个布尔值,根据这个布尔值,我们可以判断是否写入完毕,为true时,表示写入完毕,为false时,表示还没写入完毕,这时我们可以让可读流手动暂停,就能够避免数据丢失的问题。
if (writeStream.write(chunk) === false) {
readStream.pause();
}
});
// drain事件触发时,说明可以继续向流中写入数据,我们手动调整可读流的状态为流动状态,继续提供数据
writeStream.on('drain', function() {
readStream.resume();
});
readStream.on('end', (tunck) => {
console.log('主动关闭写入流');
writeStream.end();
})
这样就解决了读取和写入速度不一致导致的问题。但是我们需要不断的关注数据流的状态,需要手动操作数据流,比较繁琐。
node实现了管道机制,只需要一行代码就可以完美的实现文件的copy。pipe() 会自动管理数据流,自动监听‘data’和‘end’事件,自动控制水流速度我们不需要担心数据流的读取与写入速度。比如这样:
const fs = require('fs');
const readableStream = fs.createReadStream('./init.txt');
const writableStream = fs.createWriteStream('./init_copy.txt');
readableStream.pipe(writableStream);
无论哪一种流,都会使用.pipe()方法来实现输入和输出。pipe() 方法返回 目标流的引用,这样就可以对流进行链式地管道操作。比如下面这个文件解压操作的例子:
const fs = require("fs");
const zlib = require('zlib')
// 压缩 README.md 文件为 README.md.gz
fs.createReadStream('./README.md')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('README.md.gz'))
console.log("文件压缩完成")
一. Node.js中文文档
二. Node.js Streams 基础
三. Node中的stream (流)
四. nodejs中流(stream)的理解
五. JavaScript 标准参考教程(alpha)by阮一峰