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 有什么区别?
答案:
| 特性 | readFile | createReadStream |
|---|---|---|
| 加载方式 | 一次性加载到内存 | 流式加载(分块) |
| 内存占用 | 文件大小 | highWaterMark(默认 64KB) |
| 适用场景 | 小文件(< 100MB) | 大文件 |
| 返回类型 | Buffer/String | Readable 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 有什么区别?
答案:
| 特性 | watch | watchFile |
|---|---|---|
| 实现方式 | 操作系统事件(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();