node - 内置模块 - stream

107 阅读5分钟

在 Node.js 中,我们通常使用 readFilewriteFile 方法来读取或写入文件。这些方法虽然简单易用,但在某些场景下存在明显的局限性:

  1. 缺乏精细控制:传统方法会一次性将整个文件加载到内存中,无法实现按需读取。例如,我们无法从特定位置开始读取,也无法每次读取固定大小的字节。
  2. 无法暂停与恢复:在处理大文件时,可能需要暂停读取以执行其他任务,再继续读取。然而,传统方法无法提供这样的灵活性。
  3. 处理大文件效率低下:对大型文件进行一次性读取会占用大量内存资源,不仅影响程序性能,还可能导致内存溢出。

为了解决这些问题,Node.js 提供了「数据流(Stream)」的机制。当处理大型二进制文件时,数据流不会将整个文件一次性加载到内存中,而是将文件的数据分块读取,每次读取一小部分字节,逐步加载并处理,直到文件完全读取完毕。这种分块读取和写入的方式不仅显著提高了处理效率,还节省了内存资源。

分类

流类型名称功能描述使用场景
Readable可读流用于从数据源读取数据,例如读取文件内容。逐步获取数据,避免一次性加载整个文件。
Writable可写流用于向目标写入数据,例如写入文件。分块写入大文件或实时数据。
Duplex双工流同时支持数据的读取和写入,例如用于网络通信中的 Socket。在同一通道中实现双向数据传输。
Transform转换流在读取和写入的过程中对数据进行修改或转换。压缩数据、加密数据或改变数据格式。

只读流

字段名类型默认值描述
flagsstring'r'文件系统标志,通常为 'r' 表示读取模式。
encodingstringnull文件内容的编码,例如 'utf8''base64'
默认为 null,表示返回 Buffer。
fdnumbernull文件描述符。如果提供了 fd,则忽略 path
modenumber0o666如果文件需要创建,则设置其权限(仅对写入有效)。
autoClosebooleantrue如果为 true,当流结束或发生错误时自动关闭文件描述符。
emitClosebooleantrue如果为 true,流关闭时会触发 'close' 事件。
startnumberundefined文件读取的起始字节位置。
endnumberundefined文件读取的结束字节位置**(包含)**。
highWaterMarknumber64 * 1024 (64KB)每次读取的缓冲区大小(字节数)。
high-water-mark => 最高水位线
也就是一次可以读取的字节数
// 文件流在fs下,不在fs/promises下
import fs from 'node:fs';

let str = '';

// 读取 第5 到 第20「 包含 」 个字节的字符,每次读取 10个字节
const readStream = fs.createReadStream('./assets/users.json', {
  start: 5,
  end: 20,
  highWaterMark: 10,
});

// 监听读取事件 => 每次读取到数据时触发
readStream.on('data', (chunk) => {
  str += chunk;
});

// 监听读取结束事件 => 读取结束时触发
readStream.on('end', () => {
  console.log('读取结束');
  console.log(str);
});

// 监听读取错误事件 => 读取错误时触发
readStream.on('error', (err) => {
  console.log(err);
});

// 监听文件打开事件 => 文件打开时触发
readStream.on('open', () => {
  console.log('文件打开');
});

// 监听文件关闭事件 => 文件关闭时触发
readStream.on('close', () => {
  console.log('文件关闭');
});
import fs from 'node:fs';

const readStream = fs.createReadStream('./assets/users.json', {
  start: 5,
  end: 20,
  highWaterMark: 10,
});

readStream.on('data', (chunk) => {
  console.log('读取到的数据:', chunk.toString());
  readStream.pause(); // 暂停读取
  console.log('暂停 2 秒后继续读取...');
  setTimeout(() => {
      readStream.resume(); // 恢复读取
  }, 2000);
});

需要注意的是,当文件读取完成时,系统会自动关闭文件,你不需要手动调用close方法。

当然,如果你想在读取过程中手动关闭文件,也可以调用destroy方法。

readable.destroy();

写入流

配置项写入流(Writable Stream)备注
flags'w'(写入模式,默认值),可设置为'a'(追加写入模式)写入流可以选择覆盖写入或追加写入。
encoding'utf8'(默认值)写入流默认编码为UTF-8。
fdnull(默认值,自动分配文件描述符)文件描述符,通常不需要手动设置。
mode0o666(文件权限,默认值)文件权限,默认值为可读写权限。
autoClosetrue(写入完成后自动关闭文件,默认值)自动关闭文件的设置。
start0(从文件的第0字节开始写入,默认值)起始字节位置,写入流支持指定写入起始位置。
highWaterMark16 * 1024(每次写入的最大数据量,默认16KB)控制流的缓冲区大小,写入流默认较小。
import fs from 'node:fs';

// 创建一个写入流
const writeStream = fs.createWriteStream('./foo.txt');

// write方法可以多次调用,每次都会将数据追加到文件中
writeStream.write('Hello\t');
writeStream.write('World\n');

writeStream.write('Hello\t');
writeStream.write('Node.js\n');

// 写入流需要手动关闭 或者等脚本执行完自动关闭
// 不过node运行于服务器,一般脚本24小时运行,所以推荐手动关闭
writeStream.close();

// 监听文件打开事件 「 回调的参数是文件描述符 」
writeStream.on('open', fd => {
  console.log('文件打开', fd);
});

// 监听文件写入完成事件
writeStream.on('finish', () => {
  console.log('文件写入完成');
});

// 监听文件关闭事件
writeStream.on('close', () => {
  console.log('文件关闭');
});

// 监听文件写入错误事件
writeStream.on('error', (err) => {
  console.log(err);
});

close vs end

end 方法等同于以下两步操作:

  1. 将最后的内容写入流;
  2. 结束流并关闭底层资源(如文件描述符)。

例如:

writeStream.end('Goodbye\n');

等价于:

writeStream.write('Goodbye\n');
writeStream.end();

控制写入位置

import fs from 'node:fs';

const writeStream = fs.createWriteStream('foo.txt', { flag: 'w', start: 5 });

writeStream.write('Hello');
writeStream.write('World');

writeStream.close();

pipe

pipe 方法可理解为“管道”,用于在可读流可写流之间建立管道。这样就可以将可读流中的所有数据直接输入到对应的可写流中。

:: code-group

import fs from 'node:fs';

// 创建写入流 和 读取流
const readStream = fs.createReadStream('foo.txt');
const writeStream = fs.createWriteStream('boo_copy.txt');

// 通过读取流 建立数据传输通道
readStream.pipe(writeStream);

// 监听读取流结束事件
readStream.on('end', () => {
  console.log('拷贝完成');
});
import fs from 'node:fs';

const readStream = fs.createReadStream('foo.txt');
const writeStream = fs.createWriteStream('boo_copy.txt');

readStream.on('data', (chunk) => {
  writeStream.write(chunk);
});

// 监听读取流结束事件
readStream.on('end', () => {
  console.log('拷贝完成');
});

::