全面讲解在Node.js中使用内置fs模块进行本地文件读写操作的方法,涵盖fs/promises异步API、文件读取、文件写入与追加、异常捕获及大文件流式处理。
Node.js怎么使用fs模块读写本地文件
时间:2026-10-30
浏览:126
文章目录
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 脚本。
二、使用 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);
}
}
三、使用 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)”方式。
四、进阶处理:大文件与流式读写(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 模块,关键在于建立起现代异步思维与防错习惯:
- 统一首选
node:fs/promises:结合async/await保持代码整洁无阻塞。 - 使用
path.resolve锚定路径:永远基于模块当前目录或已知基准计算目标文件路径,彻底摆脱跨目录启动报错。 - 写入前准备目录:对不可控的目标输出路径,前置调用
fs.mkdir(dir, { recursive: true })。 - 大文件必须用流:超过数十兆的文件读取与拷贝,果断选用
createReadStream结合pipeline,维护服务的低内存占用与高并发可用性。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。