Node.js 文件操作与 Stream 流详解

小飞兽 Node.js 403 次阅读 2026-07-07

Node.js 文件系统模块(fs)

Node.js 内置的 fs 模块提供了文件系统操作能力,包括文件读写、目录操作、文件信息查询等。推荐使用 fs/promises API(基于 Promise 的异步版本),避免回调地狱。

// fs-basic.js
const fs = require('fs').promises;
const path = require('path');

async function main() {
  const dataDir = path.join(__dirname, 'data');

  // 创建目录(recursive: true 会递归创建父目录)
  await fs.mkdir(dataDir, { recursive: true });

  // 写入文件
  const content = 'Hello, Node.js 文件系统!
第二行内容';
  await fs.writeFile(
    path.join(dataDir, 'hello.txt'),
    content,
    'utf8'
  );

  // 读取文件
  const readContent = await fs.readFile(
    path.join(dataDir, 'hello.txt'),
    'utf8'
  );
  console.log('读取内容:', readContent);

  // 追加内容
  await fs.appendFile(
    path.join(dataDir, 'hello.txt'),
    '
追加的内容',
    'utf8'
  );

  // 获取文件信息
  const stats = await fs.stat(path.join(dataDir, 'hello.txt'));
  console.log('文件大小:', stats.size, '字节');
  console.log('创建时间:', stats.birthtime);
  console.log('是否文件:', stats.isFile());
  console.log('是否目录:', stats.isDirectory());

  // 列出目录内容
  const files = await fs.readdir(dataDir);
  console.log('目录内容:', files);

  // 删除文件
  await fs.unlink(path.join(dataDir, 'hello.txt'));
  console.log('文件已删除');
}

main().catch(console.error);

Stream(流)概述

Stream 是 Node.js 中处理数据的核心概念,特别适合处理大文件或数据量大的场景。流将数据分成小块(chunk)逐个处理,不需要一次性将整个文件加载到内存中。

四种流类型:

  • <strong>Readable</strong> — 可读流(如 <code>fs.createReadStream</code>、<code>http.IncomingMessage</code>)

  • <strong>Writable</strong> — 可写流(如 <code>fs.createWriteStream</code>、<code>http.ServerResponse</code>)

  • <strong>Duplex</strong> — 双工流(同时可读可写,如 <code>net.Socket</code>、<code>WebSocket</code>)

  • <strong>Transform</strong> — 转换流(在读写过程中可修改数据,如 <code>zlib.createGzip</code>)

文件流操作

// stream-copy.js
const fs = require('fs');
const path = require('path');

// 使用流复制大文件(内存效率高)
async function copyFileBig(src, dest) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(src, {
      encoding: 'utf8',      // 文件编码
      highWaterMark: 64 * 1024  // 每次读取 64KB(默认 64KB)
    });

    const writeStream = fs.createWriteStream(dest, {
      encoding: 'utf8',
      flags: 'w'  // 'a' 为追加模式
    });

    // 监听数据块
    let totalBytes = 0;
    readStream.on('data', (chunk) => {
      totalBytes += Buffer.byteLength(chunk);
      console.log(`读取 ${Buffer.byteLength(chunk)} 字节...`);
    });

    // 管道pipe:自动管理背压(backpressure)
    readStream.pipe(writeStream);

    readStream.on('end', () => {
      console.log(`复制完成,总计 ${totalBytes} 字节`);
      resolve();
    });

    readStream.on('error', (err) => {
      console.error('读取错误:', err);
      reject(err);
    });

    writeStream.on('error', (err) => {
      console.error('写入错误:', err);
      reject(err);
    });
  });
}

// 带进度显示的文件复制
async function copyWithProgress(src, dest) {
  const srcStats = await fs.promises.stat(src);
  const totalSize = srcStats.size;
  let copiedSize = 0;

  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(src);
    const writeStream = fs.createWriteStream(dest);

    readStream.on('data', (chunk) => {
      copiedSize += chunk.length;
      const percent = ((copiedSize / totalSize) * 100).toFixed(1);
      process.stdout.write(`
复制进度: ${percent}%`);
    });

    readStream.pipe(writeStream);
    readStream.on('end', () => {
      console.log('
复制完成!');
      resolve();
    });
    readStream.on('error', reject);
    writeStream.on('error', reject);
  });
}

Transform 流实战:文件压缩

// transform-gzip.js
const fs = require('fs');
const zlib = require('zlib');
const path = require('path');

// 使用 Transform 流实现 gzip 压缩
function createGzipCompress(inputPath, outputPath) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(inputPath);
    const gzipStream = zlib.createGzip();  // Transform 流
    const writeStream = fs.createWriteStream(outputPath + '.gz');

    readStream
      .pipe(gzipStream)
      .pipe(writeStream);

    writeStream.on('finish', () => {
      resolve();
    });

    readStream.on('error', (err) => {
      console.error('读取错误:', err);
      reject(err);
    });
    writeStream.on('error', reject);
    gzipStream.on('error', reject);
  });
}

// 解压
function createGzipDecompress(inputPath, outputPath) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(inputPath);
    const gunzipStream = zlib.createGunzip();  // Transform 流
    const writeStream = fs.createWriteStream(outputPath);

    readStream
      .pipe(gunzipStream)
      .pipe(writeStream);

    writeStream.on('finish', resolve);
    readStream.on('error', reject);
    writeStream.on('error', reject);
    gunzipStream.on('error', reject);
  });
}

常见问题

Q1: 读取大文件时内存不足怎么办?

使用流式读取(fs.createReadStream)而非一次性加载(fs.readFile)。流会将文件分成小块处理,默认每次 64KB,highWaterMark 选项可调整。复制或处理大文件时,始终使用 pipe 或 pipeline 连接流,让系统自动处理背压。

Q2: <code>pipe()</code> 和 <code>pipeline()</code> 有什么区别?

pipe() 不处理错误传递,任一环节出错不会自动清理其他流;pipeline() 是 Node.js 10+ 引入的 API,会自动在所有流上传递错误并调用 .destroy(),防止资源泄漏。建议始终使用 stream.pipeline()

Q3: 为什么压缩后文件变大了?

gzip 等压缩算法对重复数据效果好,但如果数据本身已高度压缩(如 JPG、MP4、ZIP 文件),gzip 压缩后反而可能略大,因为压缩头部和算法开销。可以先用 fs.stat() 判断文件类型,对已压缩文件跳过压缩。

延伸阅读

  • <a href="https://nodejs.org/zh-cn/api/fs.html">Node.js 官方文档 - fs 模块</a>
  • <a href="https://nodejs.org/zh-cn/api/stream.html">Node.js Stream 官方文档</a>
  • <a href="https://nodejs.org/zh-cn/docs/guides/backpressuring-in-streams/">理解 Node.js Streams(英)</a>