16.2 文件系统操作(fs/promises)
现代 Promise-based API 概览
Node.js 的 fs/promises 模块提供了基于 Promise 的文件系统操作 API,避免了回调地狱问题。
import { promises as fs } from 'fs';
// 或 const fs = require('fs').promises;
// 基本使用示例
async function readFileExample() {
try {
const data = await fs.readFile('example.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('读取文件出错:', err);
}
}
文件操作大全
1. 文件读写操作
文本文件读写:
async function textFileOperations() {
// 写入文件
await fs.writeFile('test.txt', 'Hello, Node.js!', 'utf8');
// 追加内容
await fs.appendFile('test.txt', '\nAdditional content', 'utf8');
// 读取文件
const content = await fs.readFile('test.txt', 'utf8');
console.log(content);
// 复制文件
await fs.copyFile('test.txt', 'test-copy.txt');
}
二进制文件操作:
async function binaryFileOperations() {
// 写入Buffer
const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
await fs.writeFile('binary.bin', buf);
// 读取为Buffer
const data = await fs.readFile('binary.bin');
console.log(data); // <Buffer 48 65 6c 6c 6f>
}
2. 文件元数据操作
async function fileMetadata() {
// 获取文件状态
const stats = await fs.stat('example.txt');
console.log(`文件大小: ${stats.size} bytes`);
console.log(`创建时间: ${stats.birthtime}`);
console.log(`是目录吗: ${stats.isDirectory()}`);
// 修改文件权限
await fs.chmod('example.txt', 0o755); // rwxr-xr-x
// 修改所有者(Unix系统)
await fs.chown('example.txt', 1000, 1000);
// 更新时间戳
const now = new Date();
await fs.utimes('example.txt', now, now);
}
3. 文件系统操作
目录操作:
async function directoryOperations() {
// 创建目录(递归创建)
await fs.mkdir('project/assets/images', { recursive: true });
// 读取目录内容
const files = await fs.readdir('project');
console.log('目录内容:', files);
// 删除目录
await fs.rmdir('empty-dir');
// 递归删除目录(Node.js 14+)
await fs.rm('project', { recursive: true, force: true });
}
文件操作:
async function advancedFileOperations() {
// 重命名/移动文件
await fs.rename('old.txt', 'new.txt');
// 创建符号链接
await fs.symlink('target.txt', 'link.txt');
// 删除文件
await fs.unlink('obsolete.txt');
// 检查文件/目录是否存在
try {
await fs.access('important.txt', fs.constants.F_OK | fs.constants.R_OK);
console.log('文件存在且可读');
} catch {
console.log('文件不可访问');
}
}
高级文件处理技巧
1. 流式读写大文件
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
async function streamLargeFile() {
const readStream = createReadStream('input.mp4');
const writeStream = createWriteStream('output.mp4');
// 使用pipeline自动处理错误和关闭流
await pipeline(
readStream,
// 可以在这里添加转换流
writeStream
);
console.log('文件复制完成');
}
2. 文件锁机制
import { open } from 'fs/promises';
async function withFileLock(filePath, callback) {
let fileHandle;
try {
// 使用排他锁打开文件
fileHandle = await open(filePath, 'wx');
return await callback(fileHandle);
} finally {
if (fileHandle) {
await fileHandle.close();
}
}
}
// 使用示例
await withFileLock('config.json', async (file) => {
await file.writeFile(JSON.stringify({ new: 'config' }, null, 2));
});
3. 文件监视与变更检测
import { watch } from 'fs/promises';
async function watchFileChanges() {
const watcher = watch('config.json', { persistent: true });
try {
for await (const event of watcher) {
console.log(`文件变更类型: ${event.eventType}`);
console.log(`变更文件: ${event.filename}`);
if (event.eventType === 'change') {
const content = await fs.readFile('config.json', 'utf8');
console.log('新内容:', content);
}
}
} catch (err) {
if (err.name === 'AbortError') {
console.log('监视已终止');
} else {
throw err;
}
}
}
// 启动监视
const controller = new AbortController();
watchFileChanges().catch(console.error);
// 停止监视
// controller.abort();
实战案例
案例1:配置文件读写器
class ConfigManager {
constructor(configPath) {
this.configPath = configPath;
this.config = {};
}
async load() {
try {
const data = await fs.readFile(this.configPath, 'utf8');
this.config = JSON.parse(data);
} catch (err) {
if (err.code === 'ENOENT') {
// 文件不存在,使用默认配置
this.config = { version: '1.0', settings: {} };
await this.save();
} else {
throw err;
}
}
return this.config;
}
async save() {
const data = JSON.stringify(this.config, null, 2);
await fs.writeFile(this.configPath, data, 'utf8');
}
async update(key, value) {
this.config[key] = value;
await this.save();
}
}
// 使用示例
async function main() {
const config = new ConfigManager('app-config.json');
await config.load();
console.log('当前配置:', config.config);
await config.update('lastUpdated', new Date().toISOString());
}
案例2:日志轮转系统
class LogRotator {
constructor(logFile, maxSize = 1024 * 1024, maxFiles = 5) {
this.logFile = logFile;
this.maxSize = maxSize;
this.maxFiles = maxFiles;
}
async writeLog(message) {
// 检查日志大小
try {
const stats = await fs.stat(this.logFile);
if (stats.size > this.maxSize) {
await this.rotateLogs();
}
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
// 写入日志
const entry = `[${new Date().toISOString()}] ${message}\n`;
await fs.appendFile(this.logFile, entry, 'utf8');
}
async rotateLogs() {
// 删除最旧的日志
const oldFile = `${this.logFile}.${this.maxFiles}`;
try {
await fs.unlink(oldFile);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
// 轮转现有日志文件
for (let i = this.maxFiles - 1; i >= 1; i--) {
const src = `${this.logFile}.${i}`;
const dest = `${this.logFile}.${i + 1}`;
try {
await fs.rename(src, dest);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
}
// 重命名当前日志
await fs.rename(this.logFile, `${this.logFile}.1`);
}
}
// 使用示例
const rotator = new LogRotator('app.log');
setInterval(async () => {
await rotator.writeLog(`Heartbeat at ${Date.now()}`);
}, 1000);
错误处理最佳实践
1. 特定错误处理
async function safeFileOperation() {
try {
await fs.writeFile('important.txt', 'data');
} catch (err) {
switch (err.code) {
case 'EACCES':
console.error('权限不足');
break;
case 'ENOSPC':
console.error('磁盘空间不足');
break;
case 'ENOENT':
console.error('目录不存在');
break;
default:
console.error('未知错误:', err);
}
}
}
2. 重试机制
async function resilientFileOperation(path, data, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await fs.writeFile(path, data);
return; // 成功则返回
} catch (err) {
if (i === retries - 1) throw err; // 最后一次重试仍失败则抛出
// 指数退避等待
const waitTime = 100 * Math.pow(2, i);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
性能优化技巧
1. 批量操作优化
async function batchFileOperations(files) {
// 并行读取多个文件
const readPromises = files.map(file =>
fs.readFile(file, 'utf8').catch(() => '')
);
const contents = await Promise.all(readPromises);
// 批量写入
const writePromises = files.map((file, i) =>
fs.writeFile(`${file}.bak`, contents[i])
);
await Promise.all(writePromises);
}
2. 使用文件描述符
async function fileDescriptorExample() {
let fd;
try {
// 打开文件获取文件描述符
fd = await fs.open('data.txt', 'w+');
// 使用文件描述符写入
await fd.writeFile('Hello World');
// 使用文件描述符读取
const buffer = Buffer.alloc(11);
const { bytesRead } = await fd.read(buffer, 0, 11, 0);
console.log(buffer.toString('utf8', 0, bytesRead));
} finally {
if (fd) await fd.close();
}
}
现代 JavaScript 特性应用
1. 使用顶层 await
// 在ES模块中可以直接使用顶层await
try {
const config = JSON.parse(await fs.readFile('config.json', 'utf8'));
console.log('应用配置:', config);
} catch (err) {
console.error('无法加载配置文件:', err);
process.exit(1);
}
2. 结合 async iterators 处理大文件
async function processLargeFile() {
const fd = await fs.open('huge-file.txt', 'r');
try {
for await (const line of fd.readLines()) {
// 逐行处理大文件
console.log('Processing line:', line);
}
} finally {
await fd.close();
}
}
总结对比表
| 操作类型 | 回调风格 (fs) | Promise风格 (fs/promises) |
|---|---|---|
| 文件读取 | fs.readFile() |
fs.readFile() |
| 文件写入 | fs.writeFile() |
fs.writeFile() |
| 目录操作 | fs.readdir() |
fs.readdir() |
| 文件状态 | fs.stat() |
fs.stat() |
| 流操作 | fs.createReadStream |
stream/promises.pipeline |
| 错误处理 | 回调参数 | try/catch |
| 代码可读性 | 回调地狱风险 | 线性异步代码 |
| 现代特性支持 | 有限 | 完全支持async/await |
通过 fs/promises API,Node.js 文件系统操作变得更加简洁和易于管理,特别是在复杂的异步流程中。结合现代 JavaScript 特性如 async/await 和 Promise 组合器,可以构建出既高效又易于维护的文件操作代码。
#前端开发
分享于 2025-03-25