跳到主要内容

Node.js 文件系统

问题

Node.js 如何操作文件系统?fs 模块有哪些常用 API?同步和异步操作有什么区别?

答案

Node.js 的 fs 模块提供了丰富的文件系统操作 API,支持同步、回调和 Promise 三种风格。


API 风格

import fs from 'fs';
import { promises as fsPromises } from 'fs';
// 或
import fs from 'fs/promises';

// 1. 同步 API(阻塞)
const data = fs.readFileSync('file.txt', 'utf8');

// 2. 回调 API(异步)
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});

// 3. Promise API(推荐)
const data = await fs.promises.readFile('file.txt', 'utf8');
推荐使用 Promise API
  • 代码更清晰,支持 async/await
  • 避免回调地狱
  • 更好的错误处理

文件读写

读取文件

import { readFile, readFileSync } from 'fs/promises';

// 读取为字符串
const text = await readFile('file.txt', 'utf8');

// 读取为 Buffer
const buffer = await readFile('file.txt');

// 读取 JSON
const json = JSON.parse(await readFile('config.json', 'utf8'));

// 大文件使用 Stream
import { createReadStream } from 'fs';
const stream = createReadStream('large-file.txt');

写入文件

import { writeFile, appendFile } from 'fs/promises';

// 写入(覆盖)
await writeFile('output.txt', 'Hello World', 'utf8');

// 写入 JSON
await writeFile('config.json', JSON.stringify(data, null, 2));

// 追加
await appendFile('log.txt', 'New log entry\n');

// 写入选项
await writeFile('file.txt', data, {
encoding: 'utf8',
mode: 0o644, // 权限
flag: 'w' // 写入模式
});

// 大文件使用 Stream
import { createWriteStream } from 'fs';
const stream = createWriteStream('output.txt');
stream.write('chunk1');
stream.write('chunk2');
stream.end();

文件描述符操作

import { open } from 'fs/promises';

// 打开文件
const fileHandle = await open('file.txt', 'r+');

try {
// 读取
const buffer = Buffer.alloc(1024);
const { bytesRead } = await fileHandle.read(buffer, 0, 1024, 0);

// 写入
await fileHandle.write('Hello', 0, 'utf8');

// 获取状态
const stat = await fileHandle.stat();

// 截断
await fileHandle.truncate(100);

} finally {
// 关闭文件
await fileHandle.close();
}

目录操作

import { mkdir, readdir, rmdir, rm } from 'fs/promises';

// 创建目录
await mkdir('new-folder');
await mkdir('nested/deep/folder', { recursive: true });

// 读取目录
const files = await readdir('src');
console.log(files); // ['index.ts', 'utils.ts', ...]

// 读取目录(含详细信息)
const entries = await readdir('src', { withFileTypes: true });
entries.forEach((entry) => {
console.log(`${entry.name}: ${entry.isDirectory() ? '目录' : '文件'}`);
});

// 删除空目录
await rmdir('empty-folder');

// 删除目录(递归,包含内容)
await rm('folder', { recursive: true, force: true });

遍历目录

import { readdir, stat } from 'fs/promises';
import { join } from 'path';

// 递归遍历目录
async function* walkDir(dir: string): AsyncGenerator<string> {
const entries = await readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = join(dir, entry.name);

if (entry.isDirectory()) {
yield* walkDir(fullPath);
} else {
yield fullPath;
}
}
}

// 使用
for await (const file of walkDir('./src')) {
console.log(file);
}

文件信息

import { stat, lstat, access, constants } from 'fs/promises';

// 获取文件信息
const stats = await stat('file.txt');

console.log({
size: stats.size, // 文件大小(字节)
isFile: stats.isFile(), // 是否是文件
isDirectory: stats.isDirectory(),
isSymbolicLink: stats.isSymbolicLink(),
createdAt: stats.birthtime, // 创建时间
modifiedAt: stats.mtime, // 修改时间
accessedAt: stats.atime, // 访问时间
mode: stats.mode, // 权限
});

// lstat 不跟随符号链接
const linkStats = await lstat('symlink');

// 检查文件是否存在
async function exists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}

// 检查权限
await access('file.txt', constants.R_OK); // 可读
await access('file.txt', constants.W_OK); // 可写
await access('file.txt', constants.X_OK); // 可执行

文件操作

import { rename, copyFile, unlink, link, symlink } from 'fs/promises';

// 重命名/移动
await rename('old.txt', 'new.txt');
await rename('file.txt', 'folder/file.txt');

// 复制
await copyFile('source.txt', 'dest.txt');
await copyFile('src.txt', 'dest.txt', constants.COPYFILE_EXCL); // 目标存在则失败

// 删除文件
await unlink('file.txt');

// 硬链接
await link('file.txt', 'hardlink.txt');

// 符号链接
await symlink('target.txt', 'symlink.txt');

// 读取符号链接目标
const target = await readlink('symlink.txt');

复制目录

import { cp } from 'fs/promises';

// Node.js 16.7.0+
await cp('src-dir', 'dest-dir', {
recursive: true,
force: true
});

监听文件变化

import { watch, watchFile, unwatchFile } from 'fs';

// watch(推荐,基于操作系统事件)
const watcher = watch('src', { recursive: true }, (eventType, filename) => {
console.log(`${eventType}: ${filename}`);
});

watcher.close();

// watchFile(轮询,跨平台一致)
watchFile('file.txt', { interval: 1000 }, (curr, prev) => {
console.log('文件已修改');
console.log('之前:', prev.mtime);
console.log('现在:', curr.mtime);
});

unwatchFile('file.txt');

// AbortController 停止监听
const controller = new AbortController();

const watcher = watch('src', {
signal: controller.signal
}, (eventType, filename) => {
console.log(eventType, filename);
});

// 停止监听
controller.abort();

常见面试问题

Q1: fs.readFile 和 fs.createReadStream 有什么区别?

答案

特性readFilecreateReadStream
加载方式一次性加载到内存流式加载(分块)
内存占用文件大小highWaterMark(默认 64KB)
适用场景小文件(< 100MB)大文件
返回类型Buffer/StringReadable Stream
可中断
// 小文件
const data = await readFile('small.txt', 'utf8');

// 大文件
const stream = createReadStream('large.txt');
for await (const chunk of stream) {
// 处理每个块
}

Q2: 如何实现文件复制?

答案

import { copyFile, createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// 方式一:copyFile(简单快速)
await copyFile('src.txt', 'dest.txt');

// 方式二:Stream(大文件,可监控进度)
async function copyWithProgress(src: string, dest: string) {
const stat = await fs.promises.stat(src);
const totalSize = stat.size;
let copiedSize = 0;

const readStream = createReadStream(src);
const writeStream = createWriteStream(dest);

readStream.on('data', (chunk) => {
copiedSize += chunk.length;
const progress = (copiedSize / totalSize * 100).toFixed(2);
console.log(`进度: ${progress}%`);
});

await pipeline(readStream, writeStream);
}

// 方式三:cp(目录复制)
await cp('src-dir', 'dest-dir', { recursive: true });

Q3: 同步和异步 API 应该如何选择?

答案

异步 API(推荐)

  • 不阻塞事件循环
  • 适用于服务端场景
  • 支持高并发

同步 API

  • 启动时加载配置
  • CLI 工具
  • 脚本任务
// ❌ 服务端使用同步 API 会阻塞
app.get('/file', (req, res) => {
const data = fs.readFileSync('large.txt'); // 阻塞!
res.send(data);
});

// ✅ 使用异步 API
app.get('/file', async (req, res) => {
const data = await fs.promises.readFile('large.txt');
res.send(data);
});

// ✅ 启动时使用同步 API 可以
const config = JSON.parse(
fs.readFileSync('config.json', 'utf8')
);

Q4: watch 和 watchFile 有什么区别?

答案

特性watchwatchFile
实现方式操作系统事件(inotify/FSEvents)轮询(stat)
性能高效较低
可靠性可能不一致一致但延迟
递归监听支持(部分平台)不支持
停止方式watcher.close()unwatchFile()
// watch - 推荐大多数场景
const watcher = watch('src', { recursive: true });

// watchFile - 网络文件系统或需要一致性
watchFile('file.txt', { interval: 1000 });

// 实际项目中使用 chokidar
import chokidar from 'chokidar';

chokidar.watch('src').on('all', (event, path) => {
console.log(event, path);
});

Q5: 如何处理大文件?

答案

// 1. 使用 Stream
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { createGzip } from 'zlib';

// 边读边写边压缩
await pipeline(
createReadStream('large.txt'),
createGzip(),
createWriteStream('large.txt.gz')
);

// 2. 逐行处理
import { createInterface } from 'readline';

const rl = createInterface({
input: createReadStream('large.csv')
});

for await (const line of rl) {
// 处理每行
}

// 3. 分块读取
const handle = await open('large.bin', 'r');
const buffer = Buffer.alloc(1024 * 1024); // 1MB

let position = 0;
while (true) {
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
if (bytesRead === 0) break;

// 处理这 1MB 数据
position += bytesRead;
}

await handle.close();

相关链接