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

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】