Node.js 文件系统 fs

104 阅读11分钟

参考资源:


Node.js 提供了强大的文件系统(File System)模块,允许开发者与文件系统进行交互。fs 模块提供了同步和异步两种操作方式,支持文件读写、目录操作、文件监听等功能。本文将深入探讨 Node.js 文件系统的各个方面,包括基础操作和高级特性。


一、文件系统基础

1.1 引入 fs 模块

在 Node.js 中,使用 require()import 引入文件系统模块:

// CommonJS 方式
const fs = require('fs');

// ES Modules 方式
import fs from 'fs';

1.2 异步与同步操作

Node.js 文件系统提供了两种操作方式:

  • 异步操作:非阻塞,使用回调函数处理结果(推荐)
  • 同步操作:阻塞,直接返回结果(适用于脚本或初始化)

最佳实践: 在生产环境中优先使用异步操作,避免阻塞事件循环。


二、文件读写操作

2.1 读取文件

API 语法:

  • fs.readFileSync(path[, options]) - 同步读取
  • fs.readFile(path[, options], callback) - 异步读取
  • fsPromises.readFile(path[, options]) - Promise 方式

参数说明:

  • path: 文件路径(字符串、Buffer 或 URL)
  • options: 选项对象或字符串编码
    • encoding: 编码格式(如 'utf8'、'ascii'、'base64'),默认 null(返回 Buffer)
    • flag: 文件标志,默认 'r'
    • signal: AbortSignal,用于取消操作
  • callback(err, data): 回调函数,data 为文件内容

常用 flag 选项:

  • 'r': 只读(默认),文件不存在则报错
  • 'r+': 读写,文件不存在则报错
  • 'rs+': 同步读写,绕过操作系统缓存
  • 'w': 写入(覆盖),文件不存在则创建
  • 'wx': 写入(排他),文件存在则失败
  • 'a': 追加,文件不存在则创建
  • 'ax': 追加(排他),文件存在则失败

返回值:

  • 同步:返回文件内容(字符串或 Buffer)
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为文件内容

2.1.1 同步操作

const fs = require('fs');

try {
  const data = fs.readFileSync('example.txt', 'utf8');
  console.log('文件内容:', data);
} catch (err) {
  console.error('读取失败:', err);
}

2.1.2 异步回调

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件内容:', data);
});

2.1.3 Promise/async-await(推荐)

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

async function readFile() {
  try {
    const data = await fsPromises.readFile('example.txt', 'utf8');
    console.log('文件内容:', data);
  } catch (err) {
    console.error('读取失败:', err);
  }
}

2.2 写入文件

API 语法:

  • fs.writeFileSync(file, data[, options]) - 同步写入
  • fs.writeFile(file, data[, options], callback) - 异步写入
  • fsPromises.writeFile(file, data[, options]) - Promise 方式

参数说明:

  • file: 文件路径(字符串、Buffer 或 URL)
  • data: 要写入的数据(字符串、Buffer、TypedArray、DataView)
  • options: 选项对象或字符串编码
    • encoding: 编码格式,默认 'utf8'
    • mode: 文件权限,默认 0o666
    • flag: 文件标志,默认 'w'
    • signal: AbortSignal,用于取消操作
  • callback(err): 回调函数

常用 flag 选项:

  • 'w': 写入(覆盖,默认)
  • 'a': 追加
  • 'r+': 读写
  • 'wx': 写入(如果文件不存在则失败)

返回值:

  • 同步:无返回值
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为 undefined

2.2.1 同步操作

const fs = require('fs');

try {
  fs.writeFileSync('output.txt', 'Hello Node.js!', 'utf8');
  console.log('写入成功');
} catch (err) {
  console.error('写入失败:', err);
}

2.2.2 异步回调

const fs = require('fs');

fs.writeFile('output.txt', 'Hello Node.js!', 'utf8', (err) => {
  if (err) {
    console.error('写入失败:', err);
    return;
  }
  console.log('写入成功');
});

2.2.3 Promise/async-await(推荐)

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

async function writeFile() {
  try {
    await fsPromises.writeFile('output.txt', 'Hello Node.js!', 'utf8');
    console.log('写入成功');
  } catch (err) {
    console.error('写入失败:', err);
  }
}

2.3 追加文件内容

API 语法:

  • fs.appendFileSync(path, data[, options]) - 同步追加
  • fs.appendFile(path, data[, options], callback) - 异步追加
  • fsPromises.appendFile(path, data[, options]) - Promise 方式

参数说明:

  • path: 文件路径(字符串、Buffer 或 URL)
  • data: 要追加的数据(字符串、Buffer)
  • options: 选项对象或字符串编码
    • encoding: 编码格式,默认 'utf8'
    • mode: 文件权限,默认 0o666
    • flag: 文件标志,默认 'a'
  • callback(err): 回调函数

返回值:

  • 同步:无返回值
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为 undefined

2.3.1 同步操作

const fs = require('fs');

try {
  fs.appendFileSync('log.txt', '新的日志条目\n', 'utf8');
  console.log('追加成功');
} catch (err) {
  console.error('追加失败:', err);
}

2.3.2 异步回调

const fs = require('fs');

fs.appendFile('log.txt', '新的日志条目\n', 'utf8', (err) => {
  if (err) {
    console.error('追加失败:', err);
    return;
  }
  console.log('追加成功');
});

2.3.3 Promise/async-await(推荐)

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

async function appendFile() {
  try {
    await fsPromises.appendFile('log.txt', '新的日志条目\n', 'utf8');
    console.log('追加成功');
  } catch (err) {
    console.error('追加失败:', err);
  }
}

三、目录操作

3.1 创建目录

API 语法:

  • fs.mkdirSync(path[, options]) - 同步创建
  • fs.mkdir(path[, options], callback) - 异步创建
  • fsPromises.mkdir(path[, options]) - Promise 方式

参数说明:

  • path: 目录路径(字符串、Buffer 或 URL)
  • options: 选项对象
    • recursive: 是否递归创建,默认 false
    • mode: 目录权限,默认 0o777
  • callback(err[, path]): 回调函数,path 为创建的目录路径

返回值:

  • 同步:返回创建的目录路径(如果 recursive 为 true)
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为创建的目录路径

3.1.1 同步操作

const fs = require('fs');

try {
  fs.mkdirSync('newDir');
  fs.mkdirSync('path/to/nested/dir', { recursive: true });
  console.log('目录创建成功');
} catch (err) {
  console.error('创建失败:', err);
}

3.1.2 异步回调

const fs = require('fs');

fs.mkdir('newDir', (err) => {
  if (err) {
    console.error('创建失败:', err);
    return;
  }
  console.log('目录创建成功');
});

3.1.3 Promise/async-await(推荐)

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

async function createDir() {
  try {
    await fsPromises.mkdir('newDir', { recursive: true });
    console.log('目录创建成功');
  } catch (err) {
    console.error('创建失败:', err);
  }
}

3.2 读取目录

API 语法:

  • fs.readdirSync(path[, options]) - 同步读取
  • fs.readdir(path[, options], callback) - 异步读取
  • fsPromises.readdir(path[, options]) - Promise 方式

参数说明:

  • path: 目录路径(字符串、Buffer 或 URL)
  • options: 选项对象或字符串编码
    • encoding: 编码格式,默认 'utf8'
    • withFileTypes: 是否返回 fs.Dirent 对象,默认 false
    • recursive: 是否递归读取(Node.js 20+),默认 false
  • callback(err, files): 回调函数,files 为文件/目录名数组

返回值:

  • 同步:返回文件/目录名数组或 fs.Dirent 对象数组
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为文件/目录名数组

3.2.1 同步操作

const fs = require('fs');

try {
  const files = fs.readdirSync('./');
  console.log('目录内容:', files);
} catch (err) {
  console.error('读取失败:', err);
}

3.2.2 异步回调

const fs = require('fs');

fs.readdir('./', (err, files) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('目录内容:', files);
});

3.2.3 Promise/async-await(推荐)

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

async function readDir() {
  try {
    const files = await fsPromises.readdir('./');
    console.log('目录内容:', files);
  } catch (err) {
    console.error('读取失败:', err);
  }
}

3.3 删除目录

API 语法:

  • fs.rmdirSync(path[, options]) - 同步删除(空目录)
  • fs.rmSync(path[, options]) - 同步删除(推荐,Node.js 14.14.0+)
  • fs.rmdir(path[, options], callback) - 异步删除(空目录)
  • fs.rm(path[, options], callback) - 异步删除(推荐)
  • fsPromises.rmdir(path[, options]) - Promise 方式(空目录)
  • fsPromises.rm(path[, options]) - Promise 方式(推荐)

参数说明:

  • path: 目录路径(字符串、Buffer 或 URL)
  • options: 选项对象
    • recursive: 是否递归删除,默认 false
    • force: 是否强制删除(忽略错误),默认 false
    • maxRetries: 最大重试次数,默认 0
    • retryDelay: 重试延迟(毫秒),默认 100
  • callback(err): 回调函数

注意: Node.js 14.14.0+ 推荐使用 fs.rm() 替代 fs.rmdir() 删除非空目录。

返回值:

  • 同步:无返回值
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为 undefined

3.3.1 同步操作

const fs = require('fs');

try {
  fs.rmdirSync('emptyDir');
  fs.rmSync('nonEmptyDir', { recursive: true });
  console.log('删除成功');
} catch (err) {
  console.error('删除失败:', err);
}

3.3.2 异步回调

const fs = require('fs');

fs.rmdir('emptyDir', (err) => {
  if (err) {
    console.error('删除失败:', err);
    return;
  }
  console.log('删除成功');
});

3.3.3 Promise/async-await(推荐)

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

async function removeDir() {
  try {
    await fsPromises.rm('nonEmptyDir', { recursive: true });
    console.log('删除成功');
  } catch (err) {
    console.error('删除失败:', err);
  }
}

3.4 删除文件

API 语法:

  • fs.unlinkSync(path) - 同步删除
  • fs.unlink(path, callback) - 异步删除
  • fsPromises.unlink(path) - Promise 方式

参数说明:

  • path: 文件路径(字符串、Buffer 或 URL)
  • callback(err): 回调函数

返回值:

  • 同步:无返回值
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为 undefined

3.4.1 同步操作

const fs = require('fs');

try {
  fs.unlinkSync('file.txt');
  console.log('删除成功');
} catch (err) {
  console.error('删除失败:', err);
}

3.4.2 异步回调

const fs = require('fs');

fs.unlink('file.txt', (err) => {
  if (err) {
    console.error('删除失败:', err);
    return;
  }
  console.log('删除成功');
});

3.4.3 Promise/async-await(推荐)

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

async function removeFile() {
  try {
    await fsPromises.unlink('file.txt');
    console.log('删除成功');
  } catch (err) {
    console.error('删除失败:', err);
  }
}

四、文件系统高级操作

4.1 文件监听

API 语法:

  • fs.watch(filename[, options][, listener]) - 监听文件/目录变化

参数说明:

  • filename: 文件或目录路径(字符串、Buffer 或 URL)
  • options: 选项对象
    • persistent: 是否持续监听,默认 true
    • recursive: 是否递归监听子目录(仅限 Windows、macOS),默认 false
    • encoding: 文件名编码,默认 'utf8'
  • listener(eventType, filename): 监听器函数
    • eventType: 事件类型('rename' 或 'change')
    • filename: 文件名(可能为 null)

返回值:

  • 返回 fs.FSWatcher 对象,可调用 .close() 关闭监听

事件类型:

  • 'rename': 文件或目录被重命名或删除
  • 'change': 文件内容发生变化

4.1.1 使用 watch 监听文件变化

const fs = require('fs');

// 监听文件
const watcher = fs.watch('example.txt', (eventType, filename) => {
  if (filename) {
    console.log(`事件类型: ${eventType}`);
    console.log(`文件名: ${filename}`);
    
    if (eventType === 'change') {
      console.log('文件内容发生变化');
      // 读取最新内容
      fs.readFile('example.txt', 'utf8', (err, data) => {
        if (!err) {
          console.log('最新内容:', data);
        }
      });
    }
  }
});

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

// 关闭监听器
setTimeout(() => {
  watcher.close();
  dirWatcher.close();
  console.log('监听器已关闭');
}, 60000); // 60秒后关闭

事件类型:

  • 'rename': 文件或目录被重命名或删除
  • 'change': 文件内容发生变化

4.1.2 使用 watchFile 监听文件(已弃用)

fs.watchFile() 使用轮询机制,性能较差,不推荐使用:

const fs = require('fs');

// 监听文件变化(轮询方式)
fs.watchFile('example.txt', { interval: 1000 }, (curr, prev) => {
  if (curr.mtime !== prev.mtime) {
    console.log('文件已修改');
    console.log('当前修改时间:', curr.mtime);
    console.log('之前修改时间:', prev.mtime);
  }
});

// 停止监听
setTimeout(() => {
  fs.unwatchFile('example.txt');
  console.log('已停止监听');
}, 30000);

注意: fs.watchFile() 已被标记为弃用,推荐使用 fs.watch()

4.2 文件权限

API 语法:

  • fs.accessSync(path[, mode]) - 同步检查权限
  • fs.access(path[, mode], callback) - 异步检查权限
  • fsPromises.access(path[, mode]) - Promise 方式
  • fs.chmodSync(path, mode) - 同步修改权限
  • fs.chmod(path, mode, callback) - 异步修改权限
  • fsPromises.chmod(path, mode) - Promise 方式

参数说明:

  • path: 文件路径(字符串、Buffer 或 URL)
  • mode: 权限模式(access)或权限值(chmod)
    • fs.constants.F_OK: 文件存在
    • fs.constants.R_OK: 可读
    • fs.constants.W_OK: 可写
    • fs.constants.X_OK: 可执行
    • chmod 使用八进制数,如 0o7550o644
  • callback(err): 回调函数

权限常量:

  • fs.constants.F_OK: 文件存在
  • fs.constants.R_OK: 可读
  • fs.constants.W_OK: 可写
  • fs.constants.X_OK: 可执行

返回值:

  • access 同步:无返回值(抛出异常表示无权限)
  • access 异步回调:无返回值
  • access Promise:返回 Promise,resolve 值为 undefined
  • chmod:无返回值

4.3.1 检查文件权限

const fs = require('fs');

// 检查文件访问权限(异步)
fs.access('file.txt', fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK, (err) => {
  if (err) {
    console.error('文件不可访问:', err);
    return;
  }
  console.log('文件可读可写');
});

// 检查文件权限(同步)
try {
  fs.accessSync('file.txt', fs.constants.R_OK);
  console.log('文件可读');
} catch (err) {
  console.error('文件不可读:', err);
}

4.3.2 修改文件权限

const fs = require('fs');

// 异步修改权限
fs.chmod('file.txt', 0o755, (err) => {
  if (err) {
    console.error('修改权限失败:', err);
    return;
  }
  console.log('权限修改成功');
});

// 同步修改权限
try {
  fs.chmodSync('file.txt', 0o644);
  console.log('权限修改成功');
} catch (err) {
  console.error('修改权限失败:', err);
}

权限模式说明:

  • 0o755: 所有者可读可写可执行,组和其他用户可读可执行
  • 0o644: 所有者可读可写,组和其他用户只读

4.3 文件统计信息

API 语法:

  • fs.statSync(path[, options]) - 同步获取统计信息
  • fs.stat(path[, options], callback) - 异步获取统计信息
  • fsPromises.stat(path[, options]) - Promise 方式
  • fs.lstatSync(path[, options]) - 同步获取链接统计信息
  • fs.lstat(path[, options], callback) - 异步获取链接统计信息
  • fsPromises.lstat(path[, options]) - Promise 方式

参数说明:

  • path: 文件路径(字符串、Buffer 或 URL)
  • options: 选项对象
    • bigint: 是否返回 BigInt 类型的时间戳,默认 false
  • callback(err, stats): 回调函数,stats 为 fs.Stats 对象

Stats 对象属性:

  • size: 文件大小(字节)
  • birthtime: 创建时间
  • mtime: 修改时间
  • atime: 访问时间
  • ctime: 状态变更时间
  • isFile(): 是否为文件
  • isDirectory(): 是否为目录
  • isSymbolicLink(): 是否为符号链接

返回值:

  • 同步:返回 fs.Stats 对象
  • 异步回调:无返回值
  • Promise:返回 Promise,resolve 值为 fs.Stats 对象

4.3.1 获取文件统计信息(stat)

const fs = require('fs');

// 异步获取文件统计信息
fs.stat('file.txt', (err, stats) => {
  if (err) {
    console.error('获取统计信息失败:', err);
    return;
  }
  
  console.log('文件统计信息:');
  console.log('是否为文件:', stats.isFile());
  console.log('是否为目录:', stats.isDirectory());
  console.log('文件大小:', stats.size, '字节');
  console.log('创建时间:', stats.birthtime);
  console.log('修改时间:', stats.mtime);
  console.log('访问时间:', stats.atime);
  console.log('权限:', stats.mode.toString(8));
});

// 同步获取文件统计信息
try {
  const stats = fs.statSync('file.txt');
  console.log('文件大小:', stats.size, '字节');
  console.log('修改时间:', stats.mtime);
} catch (err) {
  console.error('获取统计信息失败:', err);
}

4.3.2 检查文件或目录是否存在

const fs = require('fs');

// 异步检查
fs.access('file.txt', fs.constants.F_OK, (err) => {
  if (err) {
    console.log('文件不存在');
  } else {
    console.log('文件存在');
  }
});

// 同步检查(推荐)
function fileExists(filePath) {
  try {
    fs.accessSync(filePath, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

if (fileExists('file.txt')) {
  console.log('文件存在');
} else {
  console.log('文件不存在');
}

五、实用示例

5.1 目录遍历工具

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

function walkDir(dirPath, callback) {
  fs.readdir(dirPath, { withFileTypes: true }, (err, entries) => {
    if (err) {
      callback(err);
      return;
    }
    
    entries.forEach(entry => {
      const fullPath = path.join(dirPath, entry.name);
      if (entry.isDirectory()) {
        callback(null, fullPath, 'directory');
        walkDir(fullPath, callback);
      } else {
        callback(null, fullPath, 'file');
      }
    });
  });
}

// 使用示例
walkDir('./', (err, filePath, type) => {
  if (err) {
    console.error('遍历错误:', err);
    return;
  }
  console.log(`${type}: ${filePath}`);
});

5.2 日志记录工具

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

class Logger {
  constructor(logFile) {
    this.logFile = logFile;
    this.ensureLogFile();
  }
  
  ensureLogFile() {
    if (!fs.existsSync(this.logFile)) {
      fs.writeFileSync(this.logFile, '');
    }
  }
  
  log(message) {
    const timestamp = new Date().toISOString();
    const logMessage = `[${timestamp}] ${message}\n`;
    
    fs.appendFile(this.logFile, logMessage, 'utf8', (err) => {
      if (err) {
        console.error('日志写入失败:', err);
      }
    });
  }
}

// 使用示例
const logger = new Logger('app.log');
logger.log('应用程序启动');
logger.log('处理用户请求');

六、最佳实践

6.1 错误处理

始终处理文件系统操作的错误:

const fs = require('fs');

fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    if (err.code === 'ENOENT') {
      console.error('文件不存在');
    } else if (err.code === 'EACCES') {
      console.error('权限不足');
    } else {
      console.error('未知错误:', err);
    }
    return;
  }
  // 处理数据
});

6.2 路径处理

使用 path 模块处理路径,确保跨平台兼容:

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

const filePath = path.join(__dirname, 'data', 'file.txt');
fs.readFile(filePath, 'utf8', (err, data) => {
  // ...
});

6.3 性能优化

  • 优先使用异步操作,避免阻塞事件循环
  • 使用 fs.promises API 或 util.promisify 将回调转换为 Promise
const fs = require('fs');
const { promisify } = require('util');

const readFile = promisify(fs.readFile);

// 使用 async/await
async function readFileAsync() {
  try {
    const data = await readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('读取失败:', err);
  }
}

6.4 使用 Promise API

Node.js 10+ 提供了 fs.promises API:

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

async function fileOperations() {
  try {
    // 读取文件
    const data = await fs.readFile('file.txt', 'utf8');
    console.log('文件内容:', data);
    
    // 写入文件
    await fs.writeFile('output.txt', 'Hello World', 'utf8');
    
    // 创建目录
    await fs.mkdir('newDir', { recursive: true });
    
    // 读取目录
    const files = await fs.readdir('./');
    console.log('目录内容:', files);
  } catch (err) {
    console.error('操作失败:', err);
  }
}

fileOperations();

七、总结

Node.js 文件系统模块提供了丰富的功能,包括:

  • 基础操作:文件读写、目录操作
  • 高级特性:文件监听、权限管理、统计信息
  • 多种方式:同步/异步操作、回调/Promise/async-await

关键要点:

  1. 优先使用异步操作,避免阻塞事件循环
  2. 始终进行错误处理
  3. 使用 path 模块处理路径,确保跨平台兼容
  4. 考虑使用 fs.promises API 或 Promise 包装器