fs 模块(文件系统)

本文详细介绍了Node.js中的fs模块,包括异步和同步文件写入方法(writeFile、writeFileSync、appendFile、createWriteStream),读取文件(readFile、readFileSync、createReadStream),文件移动与重命名,删除文件和文件夹,以及查看资源状态。提醒读者谨慎使用相对路径,并提供实例代码演示。


在 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/ 流式写入writeFilewriteFile 会覆盖内容,需额外处理
脚本工具 / 简单场景同步 API(如readFileSync异步 API同步代码更简洁,无回调嵌套
现代项目 / 复杂流程fs.promises + async/await回调 API避免回调地狱,代码可读性更高

总结

fs模块是 Node.js 操作文件系统的核心,掌握它能让你轻松处理本地文件交互。关键总结:

  1. 路径安全:始终使用path.resolve(__dirname, '相对路径')生成绝对路径,避免相对路径陷阱。

  2. 编码风格:新手推荐fs.promises + async/await,兼顾可读性与非阻塞特性;简单脚本可用同步 API。

  3. 大文件处理:必用流式操作(createReadStream/createWriteStream),通过pipe简化数据传输。

  4. 错误处理:重点关注ENOENT(路径不存在)、EEXIST(文件已存在)、EACCES(权限不足)等常见错误,针对性处理。

  5. 实战技巧:日志切割、大文件复制、目录批量处理等场景,需结合递归、流控制、定时任务等技术组合 API。

通过本文的梳理,应对大多数文件系统操作场景。实际开发中,可根据具体需求灵活选择 API,平衡性能与开发效率。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值