Node.js 的 fs (File System) 模块 提供了与文件系统进行交互的 API。它允许 Node.js 应用程序执行各种文件操作,如读取文件、写入文件、创建目录、删除文件等。fs 模块是 Node.js 的核心模块之一,无需安装,通过 require('fs') 即可使用。

核心思想:将底层操作系统提供的文件系统操作抽象为统一的 JavaScript API,并提供同步和异步两种操作模式,以适应 Node.js 的事件驱动特性。 它是 Node.js 应用与磁盘数据交互的桥梁。


一、为什么需要 fs 模块?

在任何应用程序中,与文件系统交互都是常见的需求。无论是读取配置文件、存储用户数据、处理上传文件,还是管理日志,fs 模块都提供了基础能力:

  • 数据持久化:将应用程序的数据写入磁盘,以便长期存储。
  • 配置管理:读取和解析应用程序的配置文件。
  • 日志记录:将应用程序的运行日志写入文件。
  • 文件上传下载:处理客户端上传的文件,或提供文件下载服务。
  • 目录管理:创建、删除、遍历目录结构。
  • 系统监控:获取文件或目录的元数据(如大小、修改时间),进行监控和管理。
  • 跨平台兼容性:提供统一的 API,屏蔽了不同操作系统(Windows, Linux, macOS)在文件系统操作上的差异。

二、核心概念

在深入了解 fs 模块的具体方法之前,理解以下几个核心概念至关重要:

2.1 同步 (Synchronous) 与 异步 (Asynchronous)

fs 模块几乎所有方法都提供了同步和异步两种形式:

  • 异步方法 (Asynchronous)
    • 通常以回调函数 (callback) 作为最后一个参数,或者返回 Promise 对象 (fs/promises API)。
    • 非阻塞 (Non-blocking):当执行文件 I/O 操作时,不会暂停 Node.js 进程的事件循环,允许其他代码继续执行。文件操作完成后,通过回调函数或 Promise 解析来通知应用程序。
    • 推荐使用:在服务器端编程中,为了保持应用程序的高并发性和响应性,强烈推荐使用异步方法。
  • 同步方法 (Synchronous)
    • 通常以 Sync 结尾(例如 readFileSync)。
    • 阻塞 (Blocking):在文件 I/O 操作完成之前,会暂停 Node.js 进程的事件循环。在此期间,应用程序无法处理其他请求或执行其他代码。
    • 慎用:仅在启动脚本、小型工具或不需要高并发的场景下使用。在 Web 服务器中,同步 I/O 可能会导致性能瓶颈。

2.2 文件描述符 (File Descriptors)

  • 在类 Unix 系统中,文件描述符是一个非负整数,用于表示一个打开的文件或 I/O 资源。
  • fs 模块中,fs.open() 方法会返回一个文件描述符 (通常是 fd),后续的低级文件操作(如 fs.read()fs.write())将使用这个 fd 来指定要操作的文件。
  • 文件描述符在使用完毕后必须关闭 (fs.close()),以释放系统资源。

2.3 权限 (Permissions)

  • fs 模块支持设置和检查文件/目录的访问权限,通常遵循 POSIX 权限模型。
  • 权限通常用八进制数表示,例如 0o755 (rwxr-xr-x)。
    • 第一个数字:所有者权限。
    • 第二个数字:同组用户权限。
    • 第三个数字:其他用户权限。
    • r (read): 4, w (write): 2, x (execute): 1。

2.4 Buffer (缓冲区)

  • 在进行二进制文件读写时,Node.js 使用 Buffer 类来处理原始二进制数据。
  • fs.readFile() 默认会返回 Buffer,除非指定了编码。

三、fs 模块的核心 API

fs 模块提供了大量方法,这里我们主要介绍常用的几类。

3.1 fs/promises API (推荐用于现代 Async/Await)

从 Node.js v10.0.0 开始,fs 模块提供了基于 Promise 的 API,可以更好地与 async/await 语法结合,使异步代码更易读、易维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fs = require('fs/promises'); // 注意这里是 'fs/promises'

async function readFileContent(filePath) {
try {
const data = await fs.readFile(filePath, { encoding: 'utf8' });
console.log('文件内容 (Promise):', data);
} catch (error) {
console.error('读取文件失败 (Promise):', error);
}
}

async function writeFileContent(filePath, content) {
try {
await fs.writeFile(filePath, content, { encoding: 'utf8' });
console.log('文件写入成功 (Promise)');
} catch (error) {
console.error('写入文件失败 (Promise):', error);
}
}

// 示例用法
(async () => {
const testFilePath = 'example_promise.txt';
await writeFileContent(testFilePath, 'Hello from fs/promises!');
await readFileContent(testFilePath);
})();

在下面的示例中,我们将主要使用异步回调形式,但请记住,它们都有对应的 Sync 版本和 fs/promises 版本。

3.2 文件读取

  • fs.readFile(path[, options], callback): 异步读取整个文件内容。

  • fs.readFileSync(path[, options]): 同步读取整个文件内容。

    • path: 文件路径。
    • options: 可选对象,可包含 encoding (编码,如 ‘utf8’)、flag (文件打开模式,默认为 ‘r’)。
    • callback(err, data): 异步回调函数,data 是文件内容(Buffer 或字符串)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');

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

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

3.3 文件写入与追加

  • fs.writeFile(file, data[, options], callback): 异步写入数据到文件,如果文件不存在则创建,如果存在则覆盖。

  • fs.writeFileSync(file, data[, options]): 同步写入数据到文件。

  • fs.appendFile(file, data[, options], callback): 异步追加数据到文件。

  • fs.appendFileSync(file, data[, options]): 同步追加数据到文件。

    • file: 文件路径。
    • data: 要写入的数据(stringBuffer)。
    • options: 可选对象,可包含 encodingmode (文件权限,默认为 0o666)、flag (文件打开模式,默认为 ‘w’ 或 ‘a’)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs');

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

// 异步追加文件
fs.appendFile('output.txt', '\nAppended new line.', 'utf8', (err) => {
if (err) {
console.error('异步追加文件失败:', err);
return;
}
console.log('异步追加文件成功');
});
});

// 同步写入文件
try {
fs.writeFileSync('sync_output.txt', 'This is written synchronously.', 'utf8');
console.log('同步写入文件成功');
} catch (err) {
console.error('同步写入文件失败:', err);
}

3.4 目录操作

  • fs.mkdir(path[, options], callback): 异步创建目录。options 可包含 recursive: true 来创建嵌套目录。
  • fs.rm(path[, options], callback): 异步删除文件或目录。options 可包含 recursive: true 来递归删除目录及其内容。推荐使用 fs.rm 替代 fs.rmdir
  • fs.readdir(path[, options], callback): 异步读取目录内容(文件名数组)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const fs = require('fs');

const dirPath = 'my_new_directory';

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

// 在新目录中创建文件
fs.writeFile(`${dirPath}/file1.txt`, 'Content of file1', (err) => {
if (err) {
console.error('创建文件失败:', err);
return;
}
console.log('文件创建成功');

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

// 异步删除目录 (递归删除)
fs.rm(dirPath, { recursive: true, force: true }, (err) => {
if (err) {
console.error('删除目录失败:', err);
return;
}
console.log('目录删除成功');
});
});
});
});

3.5 文件信息 (Stats)

  • fs.stat(path[, options], callback): 异步获取文件或目录的统计信息。
  • fs.lstat(path[, options], callback): 类似于 stat,但如果路径是符号链接,则获取链接本身的统计信息,而不是它指向的目标。
  • fs.fstat(fd[, options], callback): 异步获取文件描述符的统计信息。

回调函数会接收一个 fs.Stats 对象,它提供了以下有用的方法和属性:

  • stats.isFile(): 是否为文件。
  • stats.isDirectory(): 是否为目录。
  • stats.isSymbolicLink(): 是否为符号链接。
  • stats.size: 文件大小(字节)。
  • stats.birthtime / stats.mtime / stats.ctime / stats.atime: 创建、修改、状态改变、访问时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require('fs');

fs.stat('example.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.mtime}`);
});

3.6 文件/目录重命名与删除

  • fs.rename(oldPath, newPath, callback): 异步重命名文件或目录。
  • fs.unlink(path, callback): 异步删除文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const fs = require('fs');

// 创建一个临时文件
fs.writeFile('old_name.txt', 'This is a test file.', (err) => {
if (err) throw err;
console.log('文件 old_name.txt 已创建');

// 异步重命名文件
fs.rename('old_name.txt', 'new_name.txt', (err) => {
if (err) {
console.error('重命名文件失败:', err);
return;
}
console.log('文件重命名成功');

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

3.7 文件复制

  • fs.copyFile(src, dest[, flags], callback): 异步复制文件。flags 可选,控制复制行为,如 fs.constants.COPYFILE_EXCL (如果目标存在则失败)。
1
2
3
4
5
6
7
8
9
const fs = require('fs');

fs.copyFile('source.txt', 'destination.txt', (err) => {
if (err) {
console.error('文件复制失败:', err);
return;
}
console.log('文件复制成功');
});

3.8 文件流 (Streams)

对于处理大型文件,使用 fs.createReadStream()fs.createWriteStream() 提供的文件流 (Stream) API 是更高效的方式,因为它不会一次性将整个文件加载到内存中,而是以小块数据流的形式进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fs = require('fs');
const path = require('path');

const readableStream = fs.createReadStream(path.join(__dirname, 'large_input.txt'));
const writableStream = fs.createWriteStream(path.join(__dirname, 'large_output.txt'));

// 将读取流的内容管道传输到写入流
readableStream.pipe(writableStream);

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

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

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

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

3.9 文件监控

  • fs.watch(filename[, options][, listener]): 监控文件或目录的更改。在不同操作系统上,其行为和可靠性可能有所不同,通常推荐使用更健壮的第三方库(如 chokidar)。
  • fs.watchFile(filename[, options][, listener]): 轮询 (polling) 方式监控文件更改。效率较低。
1
2
3
4
5
6
7
8
9
const fs = require('fs');

fs.watch('monitor_this_file.txt', (eventType, filename) => {
console.log(`文件 ${filename} 发生了 ${eventType} 事件`);
// eventType: 'rename' 或 'change'
});

console.log('正在监控 monitor_this_file.txt...');
// 可以在另一个终端或程序中修改 monitor_this_file.txt 来观察输出

四、错误处理

在 Node.js 中,文件系统操作的错误处理至关重要。

  • 异步回调:错误会作为回调函数的第一个参数 (err) 传递。始终检查 err 对象。
  • fs/promises:使用 try...catch 块捕获 Promise 拒绝的错误。
  • 同步方法:使用 try...catch 块捕获同步抛出的错误。

常见的 fs 错误包括:

  • ENOENT: No such file or directory (文件或目录不存在)。
  • EACCES: Permission denied (权限不足)。
  • EISDIR: Is a directory (尝试对目录执行文件操作)。
  • ENOTDIR: Not a directory (尝试对文件执行目录操作)。

五、最佳实践与安全性考虑

  1. 优先使用异步 API:这是 Node.js 的核心设计理念。使用异步方法可以避免阻塞事件循环,保持应用程序的高性能和响应性。
  2. 使用 fs/promises API:结合 async/await,它能让异步文件操作的代码更简洁、更易于理解和维护,避免回调地狱。
  3. 严格错误处理:始终检查 err 对象或使用 try...catch。未处理的错误可能导致应用程序崩溃或不可预测的行为。
  4. 路径验证和规范化
    • 防止路径遍历攻击 (Path Traversal):永远不要直接使用用户提供的路径进行文件系统操作。恶意用户可能会提供 ../../../etc/passwd 这样的路径来访问系统敏感文件。
    • 使用 path.join() 来安全地拼接路径。
    • 使用 path.resolve() 来获取绝对路径,并可能通过比较路径前缀来限制访问范围。
    • 例如,确保解析后的路径仍在允许的根目录下。
  5. 处理大型文件使用流:对于需要读取或写入大文件(例如几 MB 到 GB),请使用 fs.createReadStream()fs.createWriteStream() 来避免内存溢出。
  6. 正确的文件模式 (Flags) 和权限 (Mode)
    • flag 参数(如 'r', 'w', 'a', 'wx', 'ax')控制文件如何被打开。'wx' (写入并独占打开) 在创建新文件时很有用,可以避免覆盖现有文件。
    • mode 参数设置文件或目录的权限,如 0o755 (目录) 或 0o644 (文件)。
  7. 临时文件管理:在创建临时文件时,考虑使用 os.tmpdir() 来获取系统临时目录,并确保在使用完毕后删除这些文件。
  8. 并发操作限制:如果需要同时进行大量文件操作,考虑使用 p-limitasync.queue 等库来限制并发,防止耗尽系统资源。

六、总结

fs 模块是 Node.js 应用程序与文件系统进行交互的基石。它提供了丰富且灵活的 API,涵盖了从简单的文件读写到复杂的目录管理和文件流处理的各种需求。理解异步与同步操作的区别,优先选择异步和基于 Promise 的 API (fs/promises),并结合严格的错误处理和安全最佳实践,是构建高效、稳定和安全 Node.js 应用程序的关键。