Node.js 文件系统 fs

参考资源:


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


一、文件系统基础

1.1 引入 fs 模块

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

javascript 复制代码
// CommonJS 方式
const fs = require('fs');

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

1.2 异步与同步操作

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

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

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


二、文件读写操作

2.1 读取文件

2.1.1 异步读取文件(readFile)

fs.readFile() 是异步读取文件的推荐方式:

javascript 复制代码
const fs = require('fs');

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

// 读取二进制文件(如图片)
fs.readFile('image.png', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件大小:', data.length, '字节');
});

参数说明:

  • path: 文件路径(字符串、Buffer 或 URL)
  • encoding: 编码格式(可选,如 'utf8'、'ascii'、'base64')
  • callback: 回调函数 (err, data) => {}

2.1.2 同步读取文件(readFileSync)

fs.readFileSync() 是同步读取文件的方式:

javascript 复制代码
const fs = require('fs');

try {
  // 同步读取,会阻塞执行
  const data = fs.readFileSync('example.txt', 'utf8');
  console.log('文件内容:', data);
} catch (err) {
  console.error('读取文件失败:', err);
}

// 读取二进制文件
try {
  const buffer = fs.readFileSync('image.png');
  console.log('文件大小:', buffer.length, '字节');
} catch (err) {
  console.error('读取失败:', err);
}

注意事项:

  • 同步操作会阻塞 Node.js 事件循环
  • 适合在应用启动时读取配置文件
  • 不适合处理大文件或高并发场景

2.2 写入文件

2.2.1 异步写入文件(writeFile)

fs.writeFile() 用于异步写入文件:

javascript 复制代码
const fs = require('fs');

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

// 写入 JSON 数据
const data = { name: 'Node.js', version: '18.0.0' };
fs.writeFile('data.json', JSON.stringify(data, null, 2), 'utf8', (err) => {
  if (err) {
    console.error('写入失败:', err);
    return;
  }
  console.log('JSON 文件写入成功');
});

// 追加内容(使用 flag: 'a')
fs.writeFile('log.txt', '新的一行\n', { flag: 'a', encoding: 'utf8' }, (err) => {
  if (err) {
    console.error('追加失败:', err);
    return;
  }
  console.log('内容追加成功');
});

常用 flag 选项:

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

2.2.2 同步写入文件(writeFileSync)

fs.writeFileSync() 用于同步写入文件:

javascript 复制代码
const fs = require('fs');

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

// 追加内容
try {
  fs.writeFileSync('log.txt', '新的一行\n', { flag: 'a', encoding: 'utf8' });
  console.log('内容追加成功');
} catch (err) {
  console.error('追加失败:', err);
}

2.3 追加文件内容

除了使用 writeFileflag: 'a' 选项,还可以使用专门的追加方法:

javascript 复制代码
const fs = require('fs');

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

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

三、目录操作

3.1 创建目录

3.1.1 异步创建目录(mkdir)

javascript 复制代码
const fs = require('fs');

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

// 创建嵌套目录(需要 recursive: true)
fs.mkdir('path/to/nested/dir', { recursive: true }, (err) => {
  if (err) {
    console.error('创建目录失败:', err);
    return;
  }
  console.log('嵌套目录创建成功');
});

3.1.2 同步创建目录(mkdirSync)

javascript 复制代码
const fs = require('fs');

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

3.2 读取目录

3.2.1 异步读取目录(readdir)

fs.readdir() 用于读取目录内容:

javascript 复制代码
const fs = require('fs');
const path = require('path');

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

// 读取详细信息(使用 withFileTypes)
fs.readdir('./', { withFileTypes: true }, (err, entries) => {
  if (err) {
    console.error('读取目录失败:', err);
    return;
  }
  
  entries.forEach(entry => {
    if (entry.isDirectory()) {
      console.log(`目录: ${entry.name}`);
    } else if (entry.isFile()) {
      console.log(`文件: ${entry.name}`);
    }
  });
});

// 递归读取目录
function readDirRecursive(dirPath) {
  fs.readdir(dirPath, { withFileTypes: true }, (err, entries) => {
    if (err) {
      console.error('读取失败:', err);
      return;
    }
    
    entries.forEach(entry => {
      const fullPath = path.join(dirPath, entry.name);
      if (entry.isDirectory()) {
        console.log(`目录: ${fullPath}`);
        readDirRecursive(fullPath);
      } else {
        console.log(`文件: ${fullPath}`);
      }
    });
  });
}

readDirRecursive('./');

3.2.2 同步读取目录(readdirSync)

javascript 复制代码
const fs = require('fs');

try {
  const files = fs.readdirSync('./');
  console.log('目录内容:', files);
  
  // 读取详细信息
  const entries = fs.readdirSync('./', { withFileTypes: true });
  entries.forEach(entry => {
    console.log(entry.isDirectory() ? `目录: ${entry.name}` : `文件: ${entry.name}`);
  });
} catch (err) {
  console.error('读取目录失败:', err);
}

3.3 删除目录

3.3.1 异步删除目录(rmdir)

javascript 复制代码
const fs = require('fs');

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

// 删除非空目录(需要 recursive: true)
fs.rmdir('nonEmptyDir', { recursive: true }, (err) => {
  if (err) {
    console.error('删除目录失败:', err);
    return;
  }
  console.log('目录及其内容删除成功');
});

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

3.3.2 同步删除目录(rmdirSync)

javascript 复制代码
const fs = require('fs');

try {
  // 删除空目录
  fs.rmdirSync('emptyDir');
  console.log('目录删除成功');
  
  // 删除非空目录
  fs.rmdirSync('nonEmptyDir', { recursive: true });
  console.log('目录及其内容删除成功');
} catch (err) {
  console.error('删除目录失败:', err);
}

3.4 删除文件

javascript 复制代码
const fs = require('fs');

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

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

四、文件系统高级操作

4.1 文件监听

4.1.1 使用 watch 监听文件变化

fs.watch() 是跨平台的文件监听 API,但行为可能因平台而异:

javascript 复制代码
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() 使用轮询机制,性能较差,不推荐使用:

javascript 复制代码
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 流式文件操作

流(Stream)是处理大文件的高效方式,避免一次性将整个文件加载到内存中。

4.2.1 创建可读流

javascript 复制代码
const fs = require('fs');

// 创建可读流
const readStream = fs.createReadStream('large-file.txt', 'utf8');

readStream.on('data', (chunk) => {
  console.log('读取数据块:', chunk.length, '字节');
  // 处理数据块
});

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

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

4.2.2 创建可写流

javascript 复制代码
const fs = require('fs');

// 创建可写流
const writeStream = fs.createWriteStream('output.txt', 'utf8');

writeStream.write('第一行数据\n');
writeStream.write('第二行数据\n');
writeStream.write('第三行数据\n');

writeStream.end(); // 结束写入

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

writeStream.on('error', (err) => {
  console.error('写入错误:', err);
});

4.2.3 使用管道(pipe)复制文件

javascript 复制代码
const fs = require('fs');

// 使用管道复制大文件
const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('destination.txt');

readStream.pipe(writeStream);

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

writeStream.on('error', (err) => {
  console.error('复制错误:', err);
});

4.2.4 流式处理大文件示例

javascript 复制代码
const fs = require('fs');
const readline = require('readline');

// 逐行读取大文件
function processLargeFile(filePath) {
  const readStream = fs.createReadStream(filePath, 'utf8');
  const rl = readline.createInterface({
    input: readStream,
    crlfDelay: Infinity
  });
  
  rl.on('line', (line) => {
    // 处理每一行
    console.log('处理行:', line);
  });
  
  rl.on('close', () => {
    console.log('文件处理完成');
  });
}

processLargeFile('large-file.txt');

4.3 文件权限

4.3.1 检查文件权限

javascript 复制代码
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);
}

权限常量:

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

4.3.2 修改文件权限

javascript 复制代码
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.4 文件统计信息

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

javascript 复制代码
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);
}

Stats 对象属性:

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

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

javascript 复制代码
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 文件复制工具

javascript 复制代码
const fs = require('fs');

function copyFile(source, destination) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(source);
    const writeStream = fs.createWriteStream(destination);
    
    readStream.on('error', reject);
    writeStream.on('error', reject);
    writeStream.on('finish', resolve);
    
    readStream.pipe(writeStream);
  });
}

// 使用示例
copyFile('source.txt', 'destination.txt')
  .then(() => console.log('复制成功'))
  .catch(err => console.error('复制失败:', err));

5.2 目录遍历工具

javascript 复制代码
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.3 日志记录工具

javascript 复制代码
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 错误处理

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

javascript 复制代码
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 模块处理路径,确保跨平台兼容:

javascript 复制代码
const fs = require('fs');
const path = require('path');

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

6.3 性能优化

  • 对于大文件,使用流(Stream)而不是 readFile/writeFile
  • 优先使用异步操作,避免阻塞事件循环
  • 使用 fs.promises API 或 util.promisify 将回调转换为 Promise
javascript 复制代码
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:

javascript 复制代码
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. 处理大文件时使用流(Stream)
  3. 始终进行错误处理
  4. 使用 path 模块处理路径,确保跨平台兼容
  5. 考虑使用 fs.promises API 或 Promise 包装器
相关推荐
比老马还六3 小时前
Bipes项目二次开发/海龟编程(六)
前端·javascript
码农胖大海3 小时前
微前端架构(二):封装与实现
前端
瘦的可以下饭了3 小时前
2 数组 递归 复杂度 字符串
前端·javascript
Kellen3 小时前
ReactDOM.preload
前端·react.js
岭子笑笑3 小时前
vant 4 之loading组件源码阅读
前端
hxmmm4 小时前
自定义封装 vue多页项目新增项目脚手架
前端·javascript·node.js
ETA84 小时前
JS执行机制揭秘:你以为的“顺序执行”,其实是V8引擎在背后搞事情!
前端·javascript
鹏北海-RemHusband4 小时前
微前端实现方式:HTML Entry 与 JS Entry 的区别
前端·javascript·html
瘦的可以下饭了4 小时前
3 链表 二叉树
前端·javascript