Node.js 核心模块:fs 文件系统

fs(File System)模块是 Node.js 的核心模块之一,它提供了与文件系统进行交互的 API,包括读写文件、操作目录、修改权限、创建流等。所有文件系统操作都有同步和异步两种形式。本章将详细介绍 fs 模块的常用方法和最佳实践。

1. 引入 fs 模块

使用 require('fs') 引入模块。建议使用常量引用:

const fs = require('fs');
// 或使用 promises API(推荐)
const fsPromises = require('fs').promises;

2. 同步 vs 异步

fs 模块中的所有方法都有同步和异步两种版本。异步方法接受回调函数作为最后一个参数,同步方法则直接返回结果(或抛出异常)。在 Node.js 中,应优先使用异步方法以防止阻塞事件循环。

// 异步读取文件
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// 同步读取文件
try {
  const data = fs.readFileSync('file.txt', 'utf8');
  console.log(data);
} catch (err) {
  console.error(err);
}

// 使用 promises API(更优雅)
async function readFileAsync() {
  try {
    const data = await fsPromises.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}
推荐: 使用 fs.promises API,结合 async/await,代码更简洁且易于错误处理。

3. 读取文件

3.1 读取整个文件

const fs = require('fs').promises;

async function read() {
  // 不指定编码时返回 Buffer
  const buffer = await fs.readFile('file.txt');
  console.log(buffer.toString('utf8'));

  // 指定编码直接返回字符串
  const text = await fs.readFile('file.txt', 'utf8');
  console.log(text);
}

3.2 流式读取大文件

对于大文件,使用流可以避免一次性占用过多内存。

const fs = require('fs');
const readStream = fs.createReadStream('largefile.txt', 'utf8');

readStream.on('data', (chunk) => {
  console.log('读取到一块数据,长度:', chunk.length);
});

readStream.on('end', () => {
  console.log('文件读取完成');
});

readStream.on('error', (err) => {
  console.error('读取错误:', err);
});

4. 写入文件

4.1 覆盖写入

const fs = require('fs').promises;

async function write() {
  await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
  console.log('文件写入成功');
}

4.2 追加写入

const fs = require('fs').promises;

async function append() {
  await fs.appendFile('output.txt', '\n新的一行', 'utf8');
  console.log('内容已追加');
}

4.3 使用流写入

const fs = require('fs');
const writeStream = fs.createWriteStream('output.txt');

writeStream.write('第一行\n');
writeStream.write('第二行\n');
writeStream.end('最后一行');

writeStream.on('finish', () => {
  console.log('写入完成');
});

5. 文件与目录信息

使用 stat 方法获取文件或目录的详细信息。

const fs = require('fs').promises;

async function getStats() {
  const stats = await fs.stat('file.txt');
  console.log('是文件?', stats.isFile());
  console.log('是目录?', stats.isDirectory());
  console.log('大小:', stats.size, '字节');
  console.log('创建时间:', stats.birthtime);
  console.log('修改时间:', stats.mtime);
}

6. 目录操作

6.1 创建目录

const fs = require('fs').promises;

async function makeDir() {
  // 创建单层目录
  await fs.mkdir('newdir');
  // 递归创建多层目录
  await fs.mkdir('parent/child/grandchild', { recursive: true });
}

6.2 读取目录

const fs = require('fs').promises;

async function readDir() {
  const files = await fs.readdir('./');
  console.log('当前目录下的文件:', files);
}

6.3 删除目录

const fs = require('fs').promises;

async function removeDir() {
  // 删除空目录
  await fs.rmdir('emptydir');
  // 递归删除非空目录(Node.js 16+ 支持 recursive 选项)
  await fs.rm('nonemptydir', { recursive: true, force: true });
}

7. 重命名与移动

const fs = require('fs').promises;

async function rename() {
  await fs.rename('oldname.txt', 'newname.txt');
  console.log('重命名成功');
}

8. 删除文件

const fs = require('fs').promises;

async function unlinkFile() {
  await fs.unlink('file-to-delete.txt');
  console.log('文件已删除');
}

9. 监视文件变化

使用 fs.watchfs.watchFile 监视文件或目录的变化。

const fs = require('fs');

// 监视目录
fs.watch('./', (eventType, filename) => {
  console.log(`事件类型:${eventType},文件:${filename}`);
});

// 监视文件(轮询方式,不推荐频繁使用)
fs.watchFile('file.txt', (curr, prev) => {
  console.log('文件修改时间:', curr.mtime);
});
注意: fs.watch 在不同平台上的行为可能不一致,且并非所有平台都支持。对于关键应用,建议使用第三方库如 chokidar

10. 文件权限与所有权

const fs = require('fs').promises;

async function chmodExample() {
  // 修改权限为 rw-r--r-- (644)
  await fs.chmod('file.txt', 0o644);
}

async function chownExample() {
  // 修改所有者和组(需要 root 权限)
  await fs.chown('file.txt', uid, gid);
}

11. 文件描述符操作

对于更细粒度的控制,可以使用文件描述符(file descriptor)。

const fs = require('fs').promises;

async function fdExample() {
  const fd = await fs.open('file.txt', 'r'); // 'r' 读取模式
  const buffer = Buffer.alloc(100);
  const { bytesRead } = await fd.read(buffer, 0, 100, 0);
  console.log(`读取了 ${bytesRead} 字节:`, buffer.toString('utf8', 0, bytesRead));
  await fd.close();
}

12. 常用标志

在打开文件时,可以使用以下标志:

  • 'r' - 读取,文件不存在则抛出异常。
  • 'r+' - 读写,文件不存在则抛出异常。
  • 'w' - 写入,文件不存在则创建,存在则截断。
  • 'wx' - 排他写入,文件已存在则失败。
  • 'a' - 追加,文件不存在则创建。
  • 'ax' - 排他追加。

13. 路径处理

fs 常与 path 模块配合使用,构建安全的文件路径。

const path = require('path');
const fs = require('fs').promises;

async function safePath() {
  const filePath = path.join(__dirname, 'data', 'file.txt');
  const data = await fs.readFile(filePath, 'utf8');
  console.log(data);
}

14. 错误处理

异步方法通常通过回调或 Promise 的 catch 处理错误。常见错误码如 ENOENT(文件不存在)、EACCES(权限不足)。

const fs = require('fs').promises;

async function safeRead() {
  try {
    await fs.readFile('nonexistent.txt');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.error('文件不存在');
    } else {
      console.error('其他错误:', err);
    }
  }
}

15. 实用示例:递归遍历目录

const fs = require('fs').promises;
const path = require('path');

async function walkDir(dir) {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      await walkDir(fullPath);
    } else {
      console.log(fullPath);
    }
  }
}

walkDir('./').catch(console.error);

小结

fs 模块是 Node.js 文件操作的基石。掌握其同步/异步 API、流式操作和目录管理对于开发实用工具和后端应用至关重要。在可能的情况下,优先使用 fs.promises 和流以提高性能和代码可读性。下一章我们将介绍另一个核心模块 path,用于处理文件和目录路径。