Skip to content

Node.js 文件系统模块:轻松掌握文件操作

引言

还记得你第一次尝试用JavaScript操作电脑文件的困惑吗?在浏览器环境中,出于安全考虑,JavaScript无法直接访问本地文件系统。但Node.js改变了这一切!通过内置的fs模块,它赋予了JavaScript操作文件的强大能力,让我们能够读写文件、创建目录、监控文件变化等等。无论是构建后端服务器、开发命令行工具,还是处理数据分析,文件操作都是不可或缺的基本技能。今天,我们就一起来探索这个强大而有趣的模块!

文件系统模块基础

什么是fs模块?

fs模块是Node.js的核心模块之一,名称来源于"File System"(文件系统)的缩写。它提供了操作文件的API,简单来说,就是让你的JavaScript代码能够:

  • 创建、读取、更新和删除文件
  • 创建和管理目录
  • 查看文件信息(如大小、修改时间等)
  • 监控文件变化

首先,我们需要在代码中引入它:

javascript
// 引入fs模块
const fs = require('fs');

// 如果使用ES模块语法,则可以这样导入
// import * as fs from 'fs';

同步操作与异步操作

fs模块几乎所有操作都提供了两个版本:同步和异步。这是什么意思呢?

  • 同步操作:代码会暂停执行,直到文件操作完成。就像你排队买票,必须等前面的人买完,你才能买。
  • 异步操作:代码不会等待文件操作完成,而是继续执行后面的代码,文件操作完成后会调用一个回调函数。就像你点了外卖后继续做其他事情,外卖送到时会通知你。
javascript
// 同步读取文件(注意函数名带有Sync后缀)
try {
  const data = fs.readFileSync('hello.txt', 'utf8');
  console.log('文件内容:', data);
} catch (err) {
  console.error('读取文件时出错:', err);
}
console.log('这行代码会等到文件读取完成后才执行');

// 异步读取文件
fs.readFile('hello.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取文件时出错:', err);
    return;
  }
  console.log('文件内容:', data);
});
console.log('这行代码会立即执行,不会等待文件读取完成');

初学者提示:在实际开发中,大多数情况下应该使用异步操作,这样可以避免阻塞程序的执行,特别是在处理大文件或网络请求较多的场景下。只有在脚本启动时的配置加载等场景,才考虑使用同步操作。

常用文件操作实战

1. 读写文本文件

这是最基础也是最常用的功能:

javascript
// 写入文件(如果文件不存在会创建,存在会覆盖)
fs.writeFile('notes.txt', '这是我的第一个Node.js文件操作!', 'utf8', (err) => {
  if (err) {
    console.error('写入失败:', err);
    return;
  }
  console.log('文件写入成功!');
  
  // 读取刚刚写入的文件
  fs.readFile('notes.txt', 'utf8', (err, data) => {
    if (err) {
      console.error('读取失败:', err);
      return;
    }
    console.log('读取到的内容:', data);
  });
});

补充知识'utf8'是指定文件编码的参数,确保我们能正确处理中文等非ASCII字符。

2. 追加内容到文件

如果不想覆盖原文件,而是想在末尾添加内容:

javascript
fs.appendFile('日记.txt', '\n2023年5月1日:今天学习了Node.js的文件操作!', (err) => {
  if (err) {
    console.error('追加失败:', err);
    return;
  }
  console.log('成功追加内容到文件!');
});

3. 使用Promise和async/await简化异步操作

Node.js 10.0以后提供了fs.promisesAPI,让异步文件操作能够使用Promise和async/await,代码会更加整洁:

javascript
// 使用async/await处理文件操作
async function processFile() {
  try {
    // 写入文件
    await fs.promises.writeFile('config.json', '{"name": "Node学习笔记"}', 'utf8');
    console.log('文件写入成功');
    
    // 读取文件
    const content = await fs.promises.readFile('config.json', 'utf8');
    console.log('文件内容:', content);
    
    // 解析JSON
    const config = JSON.parse(content);
    console.log('配置名称:', config.name);
  } catch (err) {
    console.error('处理文件时出错:', err);
  }
}

processFile();

4. 目录操作

除了文件操作,fs模块还提供了目录操作的功能:

javascript
// 创建目录
fs.mkdir('uploads', (err) => {
  if (err && err.code !== 'EEXIST') {
    console.error('创建目录失败:', err);
    return;
  }
  console.log('uploads目录已创建或已存在');
});

// 读取目录内容
fs.readdir('.', (err, files) => {
  if (err) {
    console.error('读取目录失败:', err);
    return;
  }
  console.log('当前目录的文件和文件夹:');
  files.forEach(file => {
    console.log('- ' + file);
  });
});

5. 检查文件/目录是否存在与文件信息获取

在Node.js 10.0之前,常用fs.exists()检查文件是否存在,但现在推荐使用fs.stat()fs.access()

javascript
// 检查文件是否存在并获取信息
fs.stat('package.json', (err, stats) => {
  if (err) {
    if (err.code === 'ENOENT') {
      console.log('文件不存在');
    } else {
      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);
});

文件流操作:处理大文件的利器

当需要处理大文件时,一次性读取可能会占用大量内存。这时,流(Stream)成为最佳选择。流就像水管,数据可以一点一点地流过,而不需要一次性装入内存。

读取流示例

javascript
// 创建可读流
const readStream = fs.createReadStream('大视频.mp4');
let dataSize = 0;

// 监听数据块事件
readStream.on('data', (chunk) => {
  dataSize += chunk.length;
  console.log(`接收到${chunk.length}字节数据,当前已处理:${dataSize}字节`);
});

// 监听流结束事件
readStream.on('end', () => {
  console.log(`文件读取完成,共处理了${dataSize}字节的数据`);
});

// 监听错误事件
readStream.on('error', (err) => {
  console.error('读取文件流时出错:', err);
});

写入流示例

javascript
// 创建可写流
const writeStream = fs.createWriteStream('输出日志.txt');

// 多次写入数据
writeStream.write('第一行日志内容\n');
writeStream.write('第二行日志内容\n');
writeStream.write('第三行日志内容\n');

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

// 监听完成事件
writeStream.on('finish', () => {
  console.log('所有数据已写入文件');
});

// 监听错误事件
writeStream.on('error', (err) => {
  console.error('写入文件流时出错:', err);
});

管道操作:复制文件

流的强大之处在于可以通过管道(pipe)连接起来,就像连接水管一样:

javascript
// 复制文件的简便方法
const readStream = fs.createReadStream('源文件.mp4');
const writeStream = fs.createWriteStream('目标文件.mp4');

// 将读取流直接导入写入流
readStream.pipe(writeStream);

// 监听复制完成事件
writeStream.on('finish', () => {
  console.log('文件复制完成!');
});

常见错误和解决方案

在使用fs模块时,初学者经常会遇到一些常见错误:

  1. ENOENT (No such file or directory):文件或目录不存在

    javascript
    // 解决方法:在读取前检查文件是否存在
    fs.access('config.json', fs.constants.F_OK, (err) => {
      if (err) {
        console.log('文件不存在,创建新文件');
        fs.writeFile('config.json', '{}', (err) => {
          if (err) throw err;
          console.log('创建了新的配置文件');
        });
      } else {
        // 文件存在,可以安全读取
        fs.readFile('config.json', 'utf8', (err, data) => {
          if (err) throw err;
          console.log('配置文件内容:', data);
        });
      }
    });
  2. EACCES (Permission denied):权限不足

    javascript
    // 解决方法:检查并修改文件权限
    fs.chmod('重要数据.txt', 0o666, (err) => {
      if (err) {
        console.error('修改权限失败:', err);
        return;
      }
      console.log('已修改文件权限,现在可以写入');
    });
  3. 路径问题:使用相对路径时遇到的问题

    javascript
    // 解决方法:使用path模块处理路径
    const path = require('path');
    
    // 生成绝对路径
    const configPath = path.join(__dirname, 'config', 'settings.json');
    console.log('配置文件的完整路径:', configPath);
    
    fs.readFile(configPath, 'utf8', (err, data) => {
      if (err) {
        console.error('读取配置出错:', err);
        return;
      }
      console.log('配置内容:', data);
    });

实用技巧与最佳实践

  1. 使用异步操作:在生产环境中,尽量使用异步操作避免阻塞主线程。

  2. 正确处理路径:使用path模块处理路径,避免跨平台问题。

    javascript
    const path = require('path');
    const logPath = path.join(__dirname, 'logs', 'app.log');
  3. 递归创建目录:创建嵌套目录时,使用{ recursive: true }选项。

    javascript
    fs.mkdir('uploads/images/products', { recursive: true }, (err) => {
      if (err) throw err;
      console.log('嵌套目录创建成功!');
    });
  4. 使用流处理大文件:对于大于几MB的文件,建议使用流而不是一次性读写。

  5. 删除文件和目录前先确认:删除操作不可恢复,应当谨慎处理。

    javascript
    const targetDir = 'old_temp';
    
    // 先检查是否存在
    fs.access(targetDir, fs.constants.F_OK, (err) => {
      if (err) {
        console.log(`${targetDir} 不存在,无需删除`);
        return;
      }
      
      // 确认存在后再删除
      fs.rm(targetDir, { recursive: true }, (err) => {
        if (err) {
          console.error('删除目录失败:', err);
          return;
        }
        console.log(`成功删除 ${targetDir} 及其所有内容`);
      });
    });

总结与拓展

Node.js的fs模块为我们提供了强大的文件操作能力,打开了JavaScript在服务器端和本地应用开发的大门。我们学习了:

  • 基本的文件读写操作
  • 同步与异步操作的区别
  • 使用Promise简化异步操作
  • 目录的创建与管理
  • 文件流处理大文件
  • 常见错误及其解决方案

随着你的深入学习,还可以探索更多高级主题:

  • fs/promises模块的更多API
  • 文件监控(fs.watch)实现热重载
  • 使用第三方库如fs-extra扩展功能
  • 结合流与压缩库处理文件归档
  • 文件锁定机制防止并发访问冲突

希望本文能为你打开Node.js文件操作的大门,激发你深入学习的兴趣。记住,编程最好的学习方式就是实践,尝试创建一个小项目,如简易文件管理器或日志记录系统,将所学知识应用起来!

注:本文档会持续更新,欢迎关注!

用❤️分享前端技术 fedev.wiki