Node.js 文件操作与 Stream 流详解
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>