文章目录
在 Node.js 开发中,与本地文件系统交互是高频需求 —— 无论是读写配置文件、处理用户上传的文档,还是管理项目目录结构,都离不开fs模块。本文将系统梳理fs模块的核心用法,从文件读写到目录管理,结合场景化示例与现代开发实践,帮你掌握文件系统操作的精髓。
什么是 fs 模块?
fs(全称file system)是 Node.js 的内置模块,无需额外安装即可使用。它提供了一套完整的 API,用于操作计算机磁盘中的文件和目录,涵盖创建、读取、更新、删除(CRUD)等全流程。
路径使用注意事项
在文件操作中,路径的正确性直接影响程序稳定性,需特别注意:
-
相对路径:以执行 Node 命令的终端工作目录为基准(而非代码文件所在目录),容易出现路径偏移(例如在不同目录执行同一脚本)。
-
绝对路径:从磁盘根目录开始的完整路径(如
C:/project/file.txt或/user/project/file.txt),推荐优先使用。 -
最佳实践:通过
path模块的resolve方法结合__dirname(当前文件所在目录的绝对路径)生成绝对路径,例如:
const path = require("path");
const filePath = path.resolve(__dirname, "data.txt"); // 生成当前目录下data.txt的绝对路径
一、文件写入:将数据持久化到磁盘
文件写入是将内存中的数据保存到磁盘的过程,fs模块提供了 4 种常用方式,适用于不同场景。同时支持回调、同步、Promise三种编码风格,可根据项目规范选择。
1. 异步写入:writeFile(回调风格)
特点:非阻塞式操作,JavaScript 主线程不等待写入完成,适合不希望阻塞后续代码执行的场景。
语法:fs.writeFile(file, data[, options], callback)
-
file:文件路径(推荐绝对路径) -
data:待写入的数据(字符串或 Buffer) -
options:可选配置(如编码格式encoding: 'utf8',flag: 'w'默认覆盖,flag: 'wx'防止覆盖) -
callback:写入完成后的回调(参数err为错误信息,成功时为null)
示例:
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "notes.txt");
// 异步写入(防止覆盖:若文件已存在则报错)
fs.writeFile(
filePath,
"Hello Node.js",
{ encoding: "utf8", flag: "wx" },
(err) => {
if (err) {
if (err.code === "EEXIST") {
// 捕获"文件已存在"错误
console.error("写入失败:文件已存在");
} else {
console.error("写入失败:", err);
}
return;
}
console.log("写入成功!");
}
);
2. 同步写入:writeFileSync(阻塞风格)
特点:阻塞式操作,主线程会等待写入完成后再执行后续代码,适合简单场景或脚本工具(避免回调嵌套)。
语法:fs.writeFileSync(file, data[, options])(无回调,直接通过 try/catch 捕获错误)
示例:
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "notes.txt");
try {
fs.writeFileSync(filePath, "同步写入内容", "utf8");
console.log("同步写入成功!");
} catch (err) {
if (err.code === "EACCES") {
// 捕获"权限不足"错误
console.error("写入失败:无权限操作该文件");
} else {
console.error("同步写入失败:", err);
}
}
3. Promise 风格写入:fs.promises(现代异步)
Node.js 10+ 支持fs.promises模块,提供 Promise 风格 API,可配合async/await避免回调嵌套,更符合现代 JavaScript 开发习惯。
示例:
const fs = require("fs").promises;
const path = require("path");
async function writeFileDemo() {
const filePath = path.resolve(__dirname, "notes.txt");
try {
await fs.writeFile(filePath, "Promise风格的写入", "utf8");
console.log("写入成功");
} catch (err) {
if (err.code === "ENOENT") {
// 捕获"路径不存在"错误
console.error("写入失败:目标目录不存在");
} else {
console.error("写入失败:", err);
}
}
}
writeFileDemo();
4. 追加写入:appendFile(及同步 / Promise 版本)
特点:在文件末尾添加内容(而非覆盖),常用于日志记录、数据累加等场景。
示例(Promise 风格):
const fs = require("fs").promises;
const path = require("path");
async function appendLog() {
const logPath = path.resolve(__dirname, "app.log");
try {
await fs.appendFile(
logPath,
`[${new Date().toISOString()}] 用户登录\n`
);
console.log("日志追加成功");
} catch (err) {
console.error("日志写入失败:", err);
}
}
appendLog();
5. 流式写入:createWriteStream(大文件场景)
特点:分块写入数据,无需一次性加载全部内容到内存,适合大文件(如 GB 级) 或频繁写入场景(如视频上传、大数据导出)。
语法:fs.createWriteStream(path[, options])(返回一个可写流对象)
示例:
const fs = require("fs");
const path = require("path");
// 创建流式写入对象(设置高水位线:每次缓冲区满1MB时写入磁盘)
const ws = fs.createWriteStream(
path.resolve(__dirname, "large-file.txt"),
{ highWaterMark: 1024 * 1024 }
);
// 分块写入(模拟大文件内容)
let i = 0;
function write() {
let ok = true;
while (i < 10000 && ok) {
ok = ws.write(`这是第${i}行数据\n`); // 当缓冲区满时,ok为false
i++;
}
if (i === 10000) {
ws.end("写入结束"); // 所有数据写入完成后关闭流
return;
}
// 缓冲区清空后继续写入
ws.once("drain", write);
}
write();
// 监听写入完成事件
ws.on("finish", () => {
console.log("大文件写入完成!");
});
核心优势:通过 “缓冲区” 机制减少磁盘 IO 次数,大幅提升大文件处理效率。
highWaterMark参数可控制缓冲区大小(默认 64KB)。
二、文件读取:从磁盘加载数据到内存
文件读取是将磁盘中的数据加载到内存的过程,fs模块提供 3 种常用方式,需根据文件大小和场景选择。
1. 异步读取:readFile(回调风格)
特点:非阻塞式读取,适合中小型文件(内容可一次性加载到内存)。
示例:
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "notes.txt");
// 异步读取(指定编码为utf8,直接返回字符串)
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
if (err.code === "ENOENT") {
console.error("读取失败:文件不存在");
} else {
console.error("读取失败:", err);
}
return;
}
console.log("文件内容:", data);
});
2. 同步读取:readFileSync(阻塞风格)
示例:
const fs = require("fs");
const path = require("path");
const filePath = path.resolve(__dirname, "notes.txt");
try {
const data = fs.readFileSync(filePath, "utf8");
console.log("同步读取内容:", data);
} catch (err) {
console.error("同步读取失败:", err);
}
3. Promise 风格读取
const fs = require("fs").promises;
const path = require("path");
async function readFileDemo() {
const filePath = path.resolve(__dirname, "notes.txt");
try {
const data = await fs.readFile(filePath, "utf8");
console.log("Promise读取内容:", data);
} catch (err) {
console.error("读取失败:", err);
}
}
readFileDemo();
4. 流式读取:createReadStream(大文件场景)
特点:分块读取数据,每次读取固定大小(默认 64KB),适合大文件(避免一次性占用过多内存)。
示例:
const fs = require("fs");
const path = require("path");
// 创建流式读取对象(设置每次读取128KB)
const rs = fs.createReadStream(
path.resolve(__dirname, "large-file.txt"),
{ highWaterMark: 128 * 1024 }
);
// 监听数据块事件(每次读取到数据时触发)
rs.on("data", (chunk) => {
console.log(`读取到${chunk.length}字节数据`);
// 处理数据块(如解析、转发等)
// 若处理耗时,可暂停读取:rs.pause(),处理完后再rs.resume()
});
// 监听读取完成事件
rs.on("end", () => {
console.log("文件读取完毕!");
});
// 监听错误事件
rs.on("error", (err) => {
console.error("读取错误:", err);
});
典型场景:视频播放、大日志文件分析、断点续传等。
三、文件移动与重命名:rename
fs.rename(及同步 / Promise 版本)是一个 “多功能” API,既能重命名文件 / 目录,也能移动路径(跨目录、跨磁盘)。
示例(Promise 风格):
const fs = require("fs").promises;
const path = require("path");
async function moveFile() {
// 1. 重命名文件
const oldName = path.resolve(__dirname, "notes.txt");
const newName = path.resolve(__dirname, "diary.txt");
try {
await fs.rename(oldName, newName);
console.log("重命名成功!");
} catch (err) {
console.error("重命名失败:", err);
}
// 2. 移动文件到另一个目录(目标目录需存在)
const sourcePath = path.resolve(__dirname, "diary.txt");
const targetPath = path.resolve(__dirname, "docs", "diary.txt");
try {
await fs.rename(sourcePath, targetPath);
console.log("移动成功!");
} catch (err) {
console.error("移动失败:", err);
}
}
moveFile();
注意:若目标路径已存在同名文件,会被覆盖 ;移动跨磁盘文件时,可能触发复制 + 删除操作(性能较低)。
四、文件删除:unlink /rm(推荐)
删除文件可使用fs.unlink(传统方法)或fs.rm(Node.js 14.14 + 新增,更推荐)。
1. 现代方法:rm(支持更多选项)
const fs = require("fs").promises;
const path = require("path");
async function deleteFile() {
// 删除文件(force: true 忽略不存在的文件,避免报错)
try {
await fs.rm(path.resolve(__dirname, "temp.txt"), { force: true });
console.log("文件已删除");
} catch (err) {
console.error("删除失败:", err);
}
}
deleteFile();
五、文件夹操作:创建、读取与删除
目录(文件夹)操作是管理文件系统结构的基础,fs模块提供了专门的 API。
1. 创建文件夹:mkdir(支持递归)
特点:支持递归创建多级目录(如a/b/c),需指定recursive: true。
示例:
const fs = require("fs").promises;
const path = require("path");
async function createDir() {
// 递归创建多级目录
try {
await fs.mkdir(path.resolve(__dirname, "data/logs/2024"), {
recursive: true,
});
console.log("多级目录创建成功");
} catch (err) {
console.error("创建失败:", err);
}
}
createDir();
2. 读取文件夹:readdir(获取目录内容)
作用:获取目录下的所有文件和子目录名称(默认仅名称,可通过选项获取详细信息)。
示例:
const fs = require("fs").promises;
const path = require("path");
async function readDir() {
const dirPath = path.resolve(__dirname, "docs");
try {
// 读取详细信息(包含是否为文件/目录)
const items = await fs.readdir(dirPath, { withFileTypes: true });
items.forEach((item) => {
if (item.isFile()) {
console.log("文件:", item.name);
} else if (item.isDirectory()) {
console.log("目录:", item.name);
}
});
} catch (err) {
console.error("读取失败:", err);
}
}
readDir();
3. 删除文件夹:rm(支持递归删除非空目录)
Node.js 14.14 + 推荐使用fs.rm,支持recursive: true直接删除非空目录(无需手动删除内部文件)。
示例:
const fs = require("fs").promises;
const path = require("path");
async function deleteDir() {
const dirPath = path.resolve(__dirname, "data");
try {
// recursive: true 递归删除所有子内容,force: true 忽略不存在的目录
await fs.rm(dirPath, { recursive: true, force: true });
console.log("目录已删除");
} catch (err) {
console.error("删除失败:", err);
}
}
deleteDir();
六、查看资源状态:stat
fs.stat(及同步 / Promise 版本)用于获取文件或目录的详细信息(大小、创建时间、类型等)。
示例:
const fs = require("fs").promises;
const path = require("path");
async function getStats() {
const targetPath = path.resolve(__dirname, "docs");
try {
const stats = await fs.stat(targetPath);
console.log("是否为文件:", stats.isFile()); // false(若targetPath是目录)
console.log("是否为目录:", stats.isDirectory()); // true
console.log("大小:", stats.size, "字节");
console.log("最后修改时间:", stats.mtime.toLocaleString());
} catch (err) {
console.error("获取状态失败:", err);
}
}
getStats();
七、实战场景案例
将 API 与真实业务场景结合,理解 “何时用什么 API”:
场景 1:日志切割(按日期拆分日志)
需求:每天生成一个日志文件(如2024-10-20.log),避免单个日志过大。
实现思路:用createWriteStream结合定时任务,每天零点切换输出流。
const fs = require("fs");
const path = require("path");
const { format } = require("date-fns"); // 需安装date-fns:处理日期
let currentStream;
let currentDate = format(new Date(), "yyyy-MM-dd");
// 获取当前日志流
function getLogStream() {
const today = format(new Date(), "yyyy-MM-dd");
if (today !== currentDate || !currentStream) {
// 切换日期或流未初始化时,创建新流
currentDate = today;
const logPath = path.resolve(__dirname, `logs/${currentDate}.log`);
// 关闭旧流(若存在)
if (currentStream) currentStream.close();
// 创建新流(追加模式)
currentStream = fs.createWriteStream(logPath, { flags: "a" });
}
return currentStream;
}
// 写入日志
function writeLog(content) {
const stream = getLogStream();
stream.write(`[${new Date().toISOString()}] ${content}\n`);
}
// 测试:每3秒写入一条日志
setInterval(() => {
writeLog("用户访问了首页");
}, 3000);
场景 2:大文件复制(避免内存溢出)
需求:复制一个 1GB 的视频文件,避免一次性加载到内存。
实现思路:用可读流 + 可写流 “管道”(pipe)传输数据。
const fs = require("fs");
const path = require("path");
function copyLargeFile(source, target) {
const rs = fs.createReadStream(source);
const ws = fs.createWriteStream(target);
// 用pipe连接流:自动处理数据传输、缓冲区控制、错误处理
rs.pipe(ws);
// 监听完成事件
ws.on("finish", () => {
console.log("文件复制完成");
});
// 监听错误事件
rs.on("error", (err) => console.error("读取错误:", err));
ws.on("error", (err) => console.error("写入错误:", err));
}
// 复制视频文件
copyLargeFile(
path.resolve(__dirname, "source.mp4"),
path.resolve(__dirname, "target.mp4")
);
场景 3:目录批量处理(遍历并修改所有 txt 文件)
需求:递归遍历docs目录,将所有.txt文件中的 “旧内容” 替换为 “新内容”。
实现思路:结合readdir(递归遍历)+ stat(判断文件类型)+ readFile/writeFile(修改内容)。
const fs = require("fs").promises;
const path = require("path");
async function replaceInTxtFiles(dirPath, oldStr, newStr) {
const items = await fs.readdir(dirPath, { withFileTypes: true });
for (const item of items) {
const fullPath = path.resolve(dirPath, item.name);
if (item.isDirectory()) {
// 递归处理子目录
await replaceInTxtFiles(fullPath, oldStr, newStr);
} else if (item.isFile() && item.name.endsWith(".txt")) {
// 处理txt文件
const content = await fs.readFile(fullPath, "utf8");
const newContent = content.replaceAll(oldStr, newStr);
await fs.writeFile(fullPath, newContent, "utf8");
console.log(`已处理:${fullPath}`);
}
}
}
// 批量替换
replaceInTxtFiles(path.resolve(__dirname, "docs"), "旧内容", "新内容")
.then(() => console.log("所有文件处理完成"))
.catch((err) => console.error("处理失败:", err));
八、API 性能对比与选择指南
| 操作场景 | 推荐 API | 不推荐 API | 原因 |
|---|---|---|---|
| 小文件单次读写(<100KB) | readFile/writeFile | 流式操作 | 流式操作的初始化成本高于收益 |
| 大文件读写(>10MB) | 流式操作 | readFile/writeFile | 易导致内存溢出(一次性加载) |
| 频繁追加(如日志) | appendFile/ 流式写入 | writeFile | writeFile 会覆盖内容,需额外处理 |
| 脚本工具 / 简单场景 | 同步 API(如readFileSync) | 异步 API | 同步代码更简洁,无回调嵌套 |
| 现代项目 / 复杂流程 | fs.promises + async/await | 回调 API | 避免回调地狱,代码可读性更高 |
总结
fs模块是 Node.js 操作文件系统的核心,掌握它能让你轻松处理本地文件交互。关键总结:
-
路径安全:始终使用
path.resolve(__dirname, '相对路径')生成绝对路径,避免相对路径陷阱。 -
编码风格:新手推荐
fs.promises + async/await,兼顾可读性与非阻塞特性;简单脚本可用同步 API。 -
大文件处理:必用流式操作(
createReadStream/createWriteStream),通过pipe简化数据传输。 -
错误处理:重点关注
ENOENT(路径不存在)、EEXIST(文件已存在)、EACCES(权限不足)等常见错误,针对性处理。 -
实战技巧:日志切割、大文件复制、目录批量处理等场景,需结合递归、流控制、定时任务等技术组合 API。
通过本文的梳理,应对大多数文件系统操作场景。实际开发中,可根据具体需求灵活选择 API,平衡性能与开发效率。
本文详细介绍了Node.js中的fs模块,包括异步和同步文件写入方法(writeFile、writeFileSync、appendFile、createWriteStream),读取文件(readFile、readFileSync、createReadStream),文件移动与重命名,删除文件和文件夹,以及查看资源状态。提醒读者谨慎使用相对路径,并提供实例代码演示。
1627

被折叠的 条评论
为什么被折叠?



