一、什么是fs模块?

在Node.js中,文件系统(File System)是处理文件和目录的核心模块,我们通常通过fs模块来操作。想象一下,如果你用浏览器写前端代码,只能在浏览器中操作内存里的数据,但Node.js是运行在服务器端的,它可以直接和电脑的硬盘打交道,而fs模块就是Node.js提供的“硬盘操作工具包”。

比如,我们可以用它读取本地的JSON文件、创建文件夹、写入日志等。fs模块的API分为同步和异步两种,同步方法会阻塞代码执行直到完成,而异步方法则不会,更适合高并发场景。

二、同步vs异步:该选哪种?

在开始写代码前,先搞清楚同步和异步的区别:

  • 同步方法:像排队买奶茶,你必须站在那等,直到拿到奶茶才能继续。特点是简单直接,但会阻塞后续代码。
  • 异步方法:像提前点单,下单后可以去做别的事,奶茶做好了会通知你。特点是非阻塞,效率更高,适合Node.js的单线程模型。

初学者建议:优先学习异步方法,它更符合Node.js的设计理念。如果逻辑简单(比如读取小文件),也可以用同步方法快速实现。

三、基础文件读写操作

1. 引入fs模块

首先,在代码里引入fs模块,就像引入工具包一样:

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

2. 异步读取文件(readFile)

fs.readFile方法异步读取文件,语法如下:

fs.readFile('文件路径', '编码格式', (err, data) => {
  // 回调函数会在读取完成后执行
  if (err) {
    console.error('读取失败:', err);
    return; // 出错时终止后续操作
  }
  console.log('文件内容:', data);
});

参数解释
- 文件路径:可以是相对路径(如'./test.txt')或绝对路径(如'/Users/yourname/test.txt')。
- 编码格式:如'utf8'(返回字符串),不写则返回Buffer(二进制数据)。
- 回调函数:接收两个参数:err(错误信息)和data(文件内容)。

示例:创建一个test.txt文件,内容为"Hello, Node.js!",执行以下代码:

const fs = require('fs');

// 异步读取test.txt(相对路径)
fs.readFile('./test.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件内容:', data); // 输出:文件内容: Hello, Node.js!
});

3. 同步读取文件(readFileSync)

如果想让代码“顺序执行”,可以用同步方法fs.readFileSync,错误用try/catch捕获:

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

区别:同步方法会阻塞后续代码,直到文件读取完成。适合简单逻辑,比如初始化配置文件时。

4. 异步写入文件(writeFile)

fs.writeFile写入文件,默认会覆盖已有内容(如果文件不存在则自动创建):

fs.writeFile('output.txt', '这是写入的内容', (err) => {
  if (err) {
    console.error('写入失败:', err);
    return;
  }
  console.log('写入成功!');
});

示例:执行后会在当前目录生成output.txt,内容为"这是写入的内容"

注意:如果想追加内容而不是覆盖,用fs.appendFile

fs.appendFile('output.txt', '\n这是追加的内容', (err) => {
  if (err) {
    console.error('追加失败:', err);
    return;
  }
  console.log('追加成功!');
});

5. 路径处理:避免“找不到文件”的坑

路径是文件操作的关键,Node.js中path模块可以帮我们更安全地处理路径(如跨平台拼接路径)。先简单引入path模块:

const path = require('path');

常用路径方法
- path.join(__dirname, 'subdir', 'file.txt'):拼接路径,自动处理/\(Windows系统自动转换)。
- __dirname:当前模块所在目录的绝对路径(动态变量,永远指向当前文件所在文件夹)。
- __filename:当前模块的绝对路径(包含文件名)。

示例:用path.join拼接路径:

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

// 拼接路径:__dirname + 'data' + 'test.txt'
const filePath = path.join(__dirname, 'data', 'test.txt');

// 读取该路径下的文件
fs.readFile(filePath, 'utf8', (err, data) => {
  if (err) {
    console.error('路径错误:', err);
    return;
  }
  console.log('正确读取到文件:', data);
});

四、目录操作:创建、读取、删除

1. 创建目录(mkdir)

fs.mkdir创建目录,默认只创建最外层目录,嵌套目录需要递归创建:

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

递归创建多级目录:需要加recursive: true参数:

fs.mkdir('./a/b/c', { recursive: true }, (err) => { // 允许创建嵌套目录
  if (err) {
    console.error('创建多级目录失败:', err);
    return;
  }
  console.log('多级目录创建成功!');
});

2. 读取目录内容(readdir)

fs.readdir列出目录下的所有文件和子目录:

fs.readdir('./newDir', (err, files) => {
  if (err) {
    console.error('读取目录失败:', err);
    return;
  }
  console.log('目录内容:', files); // 输出数组,如 ['file1.txt', 'subDir']
});

3. 删除目录(rmdir)

fs.rmdir删除目录,仅支持删除空目录

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

五、文件信息与权限检查

1. 检查文件是否存在(existsSync)

fs.existsSync快速检查文件/目录是否存在(同步方法):

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

const filePath = path.join(__dirname, 'test.txt');

if (fs.existsSync(filePath)) {
  console.log('文件存在!');
  fs.readFile(filePath, 'utf8', (err, data) => { /* ... */ });
} else {
  console.log('文件不存在!');
}

2. 获取文件/目录信息(stat)

fs.stat获取文件详细信息(大小、创建时间、是否为目录等):

fs.stat('./test.txt', (err, stats) => {
  if (err) {
    console.error('获取信息失败:', err);
    return;
  }
  console.log('是否为文件:', stats.isFile()); // true
  console.log('文件大小:', stats.size, '字节'); // 15字节(示例)
});

六、常见问题与解决

1. 文件路径错误怎么办?

原因:相对路径的基准是“执行node命令的目录”,而非脚本所在目录。
解决:用__dirname(当前脚本所在目录)拼接路径,或直接用绝对路径。

// 错误示例(可能找不到文件)
fs.readFile('test.txt', ...); // 依赖执行node命令的位置

// 正确示例
fs.readFile(path.join(__dirname, 'test.txt'), ...); // 基于脚本位置的绝对路径

2. 如何处理大文件?

小文件用readFile/writeFile即可,但大文件(如视频、日志)会占用内存。此时推荐使用流(Stream),它会分块读取数据:

const fs = require('fs');
const readStream = fs.createReadStream('largeFile.txt');
const writeStream = fs.createWriteStream('copiedFile.txt');

// 管道流:数据从readStream流向writeStream
readStream.pipe(writeStream);

七、总结

fs模块是Node.js操作文件系统的基石,掌握它你就能实现:

  • 读取配置文件、日志文件
  • 创建/删除目录、文件
  • 处理文件内容(读写、追加、复制)

实践建议
1. 先从简单的“创建文件→写入内容→读取内容→删除文件”开始
2. 尝试用path模块处理不同平台的路径问题
3. 对比同步/异步方法的区别,理解事件循环和非阻塞I/O的优势

记住,编程学习没有捷径,多敲代码、多踩坑、多调试,才能真正掌握文件系统操作的精髓!

小夜