Node.js怎么使用fs模块读写本地文件

作者:夜航船时间:2026-10-30 18:46:55浏览:128

全面讲解在Node.js中使用内置fs模块进行本地文件读写操作的方法,涵盖fs/promises异步API、文件读取、文件写入与追加、异常捕获及大文件流式处理。

在 Node.js 中操作本地文件,核心是使用官方内置的 fs(File System)模块。无论是读取配置、解析本地数据文件,还是写入日志、生成结果文件,Node.js 都提供了完善的 API 支持。目前现代 Node.js 开发中最推荐的做法是使用基于 Promise 的 node:fs/promises,它能通过 async/await 语法写出清晰、安全且不阻塞主线程的代码。

一、认识 fs 模块的三种 API 风格

fs 模块作为 Node.js 最早期的内置模块之一,经历了多次演进。在编写文件操作代码前,必须明确当前模块支持的三种调用方式及其适用场景:

  • Promise 风格(node:fs/promises):现代 Node.js 项目的标准选择,返回 Promise 对象,配合 async/await 使用,语法平铺直观且便于统一错误捕获。
  • 回调函数风格(node:fs 异步方法):传统的异步实现,遵循 Error-First 回调规范 (err, data) => {}。虽然非阻塞,但在多步骤嵌套操作时容易陷入回调地狱。
  • 同步方法(以 Sync 结尾的方法):直接返回结果并阻塞整个 Node.js 事件循环。在所有并发请求停止等待的情况下执行,通常仅用于服务启动阶段读取基础静态配置或简单的 CLI 脚本。
Node.js怎么使用fs模块读写本地文件
图 1:Node.js fs 模块三种 API 调用风格与底层机制对比

二、使用 fs 模块读取本地文件

读取文件是日常开发中最频繁的需求。为了保证路径在任何工作目录下都能准确定位,我们必须配合 path 模块构建绝对路径,避免由于终端执行目录不同而引发找不到文件的错误。

1. 引入模块与路径准备

在 CommonJS 与 ES 模块(ESM)规范中,引入路径的方式略有差异:

// ES Module 语法(推荐)
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 拼接目标文件的安全绝对路径
const targetPath = path.resolve(__dirname, 'data.txt');

2. 异步读取文本文件

调用 fs.readFile(filePath, options)。必须注意的是:若不传入编码选项,默认将返回 Node.js 的 Buffer 二进制数据流;如果需要直接获得字符串,需显式声明 'utf-8' 编码:

async function readTextFile() {
  try {
    const filePath = path.resolve(__dirname, 'example.txt');
    // 传入 'utf-8' 编码,直接返回字符串
    const content = await fs.readFile(filePath, 'utf-8');
    console.log('文件内容:', content);
    return content;
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.error('指定文件不存在,请检查路径:', error.path);
    } else if (error.code === 'EACCES') {
      console.error('读取权限不足,拒绝访问:', error.path);
    } else {
      console.error('读取文件发生未知错误:', error.message);
    }
  }
}

3. 读取并解析 JSON 文件

日常项目中经常需要读取本地 JSON 配置文件。读取后结合 JSON.parse 即可完成数据反序列化:

async function loadConfig() {
  try {
    const configPath = path.resolve(__dirname, 'config.json');
    const rawData = await fs.readFile(configPath, 'utf-8');
    const config = JSON.parse(rawData);
    console.log('读取配置成功,端口号:', config.port);
    return config;
  } catch (error) {
    console.error('配置文件读取或解析失败:', error.message);
  }
}
Node.js怎么使用fs模块读写本地文件
图 2:本地文件读取的标准操作步骤与异常分支流程

三、使用 fs 模块写入与追加文件

写入操作分为覆盖写入与追加写入两种主要模式。在写入前,如果目标目录不存在,直接执行写入会触发 ENOENT 错误,因此在生产环境中,通常应先确保目录已经创建。

1. 确保目标目录存在(递归创建)

在 Node.js 中,可以使用 fs.mkdir 并配置 { recursive: true } 参数。无论目标深层目录是否存在,该命令都会安全创建,不会因目录已存在而报错:

async function ensureDirectory(dirPath) {
  // recursive: true 类似 Linux 中的 mkdir -p
  await fs.mkdir(dirPath, { recursive: true });
}

2. 覆盖写入:writeFile

fs.writeFile(file, data, options) 会将指定内容写入文件。如果文件不存在则新建;如果文件已存在,则会彻底清空旧内容并覆盖:

async function saveUserData(userData) {
  try {
    const outputDir = path.resolve(__dirname, 'output');
    await fs.mkdir(outputDir, { recursive: true });

    const targetFile = path.resolve(outputDir, 'user.json');
    const content = JSON.stringify(userData, null, 2);

    // 默认 flag 为 'w'(清空并覆盖)
    await fs.writeFile(targetFile, content, 'utf-8');
    console.log('数据已成功写入:', targetFile);
  } catch (error) {
    console.error('写入文件失败:', error.message);
  }
}

3. 追加写入:appendFile

在记录日志或累积写入时,需要保留原文件内容并把新数据添加到末尾,此时应当使用 fs.appendFile:

async function recordLog(message) {
  try {
    const logFile = path.resolve(__dirname, 'app.log');
    const timestamp = new Date().toISOString();
    const logLine = `[${timestamp}] ${message}\n`;

    // 追加写入内容到文件末尾
    await fs.appendFile(logFile, logLine, 'utf-8');
  } catch (error) {
    console.error('记录日志失败:', error.message);
  }
}
注意事项:writeFile 与 appendFile 会把传入的完整数据一次性提交给系统内核处理。若需一次性写入几百兆以上的超大型数据,应使用后文介绍的“流(Stream)”方式。
Node.js怎么使用fs模块读写本地文件
图 3:文件写入的核心模式与参数配置关系

四、进阶处理:大文件与流式读写(Stream)

当处理数百兆甚至数吉字节(GB)的文件时,若直接使用 fs.readFile 或 fs.writeFile,Node.js 会尝试把整份文件一口气载入内存中。这不仅会造成内存飙升,还很容易触发 V8 堆内存溢出(JavaScript heap out of memory)导致服务崩溃。

针对大文件场景,必须使用流(Stream)进行分块渐进处理:

import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
import path from 'node:path';

async function copyLargeFile() {
  const sourceFile = path.resolve(__dirname, 'huge-dataset.csv');
  const destFile = path.resolve(__dirname, 'backup-dataset.csv');

  // 创建可读流与可写流(分块载入内存,默认 chunk 为 64KB)
  const readStream = fs.createReadStream(sourceFile);
  const writeStream = fs.createWriteStream(destFile);

  try {
    // 使用 pipeline 安全连接流,自动管理背压与异常销毁
    await pipeline(readStream, writeStream);
    console.log('大文件复制完成,全程维持极低内存消耗');
  } catch (error) {
    console.error('流处理管道发生异常:', error.message);
  }
}

使用流的核心优势在于流式背压控制(Backpressure):当写入端速度慢于读取端时,Node.js 会自动暂停读取流,待写入缓冲区释放后再恢复读取,始终将内存占用控制在几十千字节(KB)以内。

五、常用文件属性与状态判断

很多开发者在操作文件前习惯先调用检查方法查看文件是否存在。在实际工程中,需要规避检查与操作之间的时序竞争(TOCTOU 竞态陷阱):

  • 已被废弃的 API:严禁使用 fs.exists(),该方法早被废弃,因为它脱离了后续操作的原子性。
  • 最佳实践:对于常规读写,直接尝试执行操作并捕获错误(例如捕获 ENOENT 错误),这远比先检查后操作更安全可靠。
  • 获取元信息:如果确实需要获取文件大小、修改时间或判断是文件还是目录,使用 fs.stat:
async function checkFileInfo(filePath) {
  try {
    const stats = await fs.stat(filePath);
    console.log('是普通文件:', stats.isFile());
    console.log('是目录:', stats.isDirectory());
    console.log('文件大小(Bytes):', stats.size);
    console.log('最后修改时间:', stats.mtime);
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.log('文件或目录不存在');
    }
  }
}
Node.js怎么使用fs模块读写本地文件
图 4:Node.js 文件操作技术选型与避坑防线总结

六、总结备忘

掌握 Node.js 的 fs 模块,关键在于建立起现代异步思维与防错习惯:

  1. 统一首选 node:fs/promises:结合 async/await 保持代码整洁无阻塞。
  2. 使用 path.resolve 锚定路径:永远基于模块当前目录或已知基准计算目标文件路径,彻底摆脱跨目录启动报错。
  3. 写入前准备目录:对不可控的目标输出路径,前置调用 fs.mkdir(dir, { recursive: true })。
  4. 大文件必须用流:超过数十兆的文件读取与拷贝,果断选用 createReadStream 结合 pipeline,维护服务的低内存占用与高并发可用性。
作者最新文章
相关文章
更多