{T}

fs 文件系统模块

fs 模块是 Node.js 中内置的文件系统模块,使用它可以对本地的文件及目录(文件夹)进行操作。fsfile system 的简写,表示文件系统。

javascript
const fs = require("fs")

模块特性

核心功能

  • 文件操作:读取、写入、删除、复制、重命名、截断文件
  • 目录操作:创建、读取、删除目录,获取目录信息
  • 流操作:支持流式读写,适合处理大文件
  • 文件监控:实时监控文件和目录的变化
  • 权限管理:修改文件权限和所有权
  • 符号链接:创建和读取符号链接
  • 文件描述符:底层文件操作接口

跨平台兼容性

fs 模块提供了跨平台的文件系统操作接口,但需要注意:

  • 路径分隔符:Windows 使用 \,Unix/Linux/macOS 使用 /(建议使用 path 模块)
  • 权限模式:Windows 和 Unix 权限模型不同
  • 文件监控:不同平台的文件系统事件机制有差异
  • 符号链接:Windows 需要管理员权限创建符号链接

性能考虑

API 风格性能特点适用场景
同步 API阻塞事件循环,性能较低启动脚本、CLI 工具、简单脚本
回调 API异步非阻塞,性能良好兼容旧代码、事件驱动场景
Promise API异步非阻塞,代码清晰现代应用、推荐使用

三种 API 风格

fs 模块提供了三种不同的 API 风格:

  1. 回调函数风格(Callback):传统的异步 API,使用回调函数处理结果
  2. 同步风格(Sync):同步 API,会阻塞事件循环
  3. Promise 风格(Promises):基于 Promise 的异步 API(推荐)
javascript
// 回调函数风格
fs.readFile("file.txt", "utf8", (err, data) => {
  if (err) throw err
  console.log(data)
})

// 同步风格
try {
  const data = fs.readFileSync("file.txt", "utf8")
  console.log(data)
} catch (err) {
  console.error(err)
}

// Promise 风格(推荐)
const fsPromises = require("fs").promises
fsPromises
  .readFile("file.txt", "utf8")
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

// 或使用 async/await
async function readFile() {
  try {
    const data = await fsPromises.readFile("file.txt", "utf8")
    console.log(data)
  } catch (err) {
    console.error(err)
  }
}

同步 vs 异步的选择策略

在实际开发中,选择同步还是异步方法是一个常见的决策点。以下是推荐的选择策略:

场景推荐方式原因
应用启动时加载配置文件同步只执行一次,不影响运行时性能
一次性读取小型静态资源同步简单直接,代码可读性好
处理用户请求中的文件操作异步避免阻塞事件循环,保持响应能力
大文件读写操作异步防止长时间阻塞,影响整体性能
高并发场景下的文件操作异步必须异步,否则严重影响吞吐量
CLI 工具脚本同步或异步均可取决于是否需要并行处理

选择建议

  • 对于一次性加载、不会二次读取的内容(如项目启动时读取配置文件),可以使用同步方法
  • 对于体积较大、可能需要多次读写的文件,或者可能影响业务流程响应速度的操作,应使用异步方式
  • 如果不确定选择哪种方式,优先选择异步方式,这是更安全的选择

同步方法的错误处理

使用同步方法时,务必使用 try-catch 捕获错误,避免程序崩溃:

javascript
let data
try {
  data = fs.readFileSync('./config.json')
} catch (err) {
  console.log('配置文件读取出错,及时检查服务')
  // 其他错误处理,如 log(err)、通知管理员等
}

从回调地狱到 async/await 的演进

当使用异步回调方式编写代码时,如果多个异步操作存在依赖关系,就会面临"回调地狱"(Callback Hell)的问题:

javascript
// 回调地狱示例
fs.readFile('./cfg1.json', function (err, data1) {
  fs.readFile(data1.cfgPath, function (err, data2) {
    fs.readFile(data2.cfgPath, function (err, data3) {
      fs.readFile(data3.cfgPath, function (err, data4) {
        fs.readFile(data4.cfgPath, function (err, data5) {
          // 嵌套层级过深,难以维护
        })
      })
    })
  })
})

这种代码不仅编写困难,维护成本也很高。Node.js 社区推动了多种解决方案的发展,包括 Promise 规范和 async/await 语法。

使用 Promise 改造

javascript
// 将 readFile 封装为 Promise
const readFile = filePath => new Promise((resolve, reject) => {
  fs.readFile(filePath, (err, data) => {
    if (err) reject(err)
    else resolve(data)
  })
})

// 使用 Promise 链式调用
readFile('./cfg1.json')
  .then(data => readFile(data.cfgPath))
  .then(data => readFile(data.cfgPath))
  .then(data => readFile(data.cfgPath))
  .then(data => {
    console.log('最终数据:', data)
  })
  .catch(err => console.error('读取失败:', err))

Promise 链式调用解决了嵌套问题,但长链式结构仍然不够直观,且难以在后续操作中访问前面步骤的数据。

使用 async/await 改造(推荐)

javascript
const readFile = filePath => new Promise((resolve, reject) => {
  fs.readFile(filePath, (err, data) => {
    if (err) reject(err)
    else resolve(data)
  })
})

// 在 async 函数中使用 await
async function readConfig() {
  try {
    const data1 = await readFile('./cfg1.json')
    const data2 = await readFile(data1.cfgPath)
    const data3 = await readFile(data2.cfgPath)
    const data4 = await readFile(data3.cfgPath)
    console.log(data4)
    return data4
  } catch (err) {
    console.error('配置读取失败:', err)
    throw err
  }
}

readConfig()

async/await 让异步代码看起来像同步代码,既保持了异步的非阻塞特性,又获得了同步代码的可读性。fs 模块的任何异步方法都可以用这种方式包装使用。

文件的读取与写入

检查文件是否存在 access

检查文件是否存在 fs 模块内置许多方法,用以对文件进行相关操作。具体使用时,有的方法如果发现文件不存在,可以创建文件,而有的方法则不能,这时就会出现错误,为了避免这类错误,在对文件进行操作之前,一般都需要检测文件是否存在,并且根据需要检查文件的可读或可写等属性。

检查文件是否存在及其属性可以通过 access() 方法实现,语法格式如下:

  • path:文件的路径
  • mode:要执行的可访问性检查,默认值为 fs.constants.F_OK。查看文件访问常量以获取可能的 mode
  • callback:回调函数,使用一个可能的错误参数进行调用。如果检查可访问性失败,则错误参数将是 Error 对象,常见的 Error 对象值见下表
javascript
fs.access(path, mode, callback)
mode 常量说明
F_OK指示文件对调用进程可见的标志。这对于确定文件是否存在很有用,但没有说明 rwx 权限
R_OK指示文件可以被调用进程读取的标志
W_OK指示文件可以被调用进程写入的标志
X_OK指示文件可以被调用进程执行的标志,在 Windows 系统中等效于 fs.constants.F_OK

常见 Error 对象值及说明:

<img src="https://zhangzhengyang.oss-cn-beijing.aliyuncs.com/images/202501122105408.png" alt="image-20250112210542543" style="zoom:67%;" />
WARNING

fs 模块提供对文件与目录进行操作的方法时,通常分别提供同步方法和异步方法,其中,同步方法名通常是在异步方法名后面加了 Sync 后缀,如 access() 方法的对应同步方法为 accessSync(),但除了文件读写操作,一般都默认使用异步方法

javascript
const fs = require("fs")

// 查看 demo.txt 文件是否存在
fs.access("demo.txt", fs.constants.F_OK, (err) => {
  if (err) {
    console.log("demo.txt 文件不存在")
  } else {
    console.log("demo.txt 文件存在")
  }
})

除此之外,还可以检查文件的相关属性。如要检查文件是否可读,可以将 fs.constants.F_OK 修改为 fs.constants.R_OK;而如果要检查文件是否可写,则可以将 fs.constants.F_OK 修改为 fs.constants.W_OK。另外 access() 方法的 mode 参数也可以同时设置多个值,如果 mode 参数有多个值,中间用 | 分割。例如,检查 demo.txt 文件是否存在且是否可写的代码如下:

javascript
const fs = require("fs")

// 查看 demo.txt 文件是否存在且可写
fs.access("demo.txt", fs.constants.F_OK | fs.constants.W_OK, (err) => {
  if (err) {
    console.log(err)
    if (err.code === "ENOENT") {
      console.log("demo.txt 文件不存在")
    } else if (err.code === "EPERM") {
      console.log("demo.txt 文件存在,但不可写")
    } else {
      console.log("未知错误")
    }
  } else {
    console.log("demo.txt 存在,并且可写")
  }
})

使用 Promise 风格

javascript
const fsPromises = require("fs").promises

async function checkFile() {
  try {
    await fsPromises.access("demo.txt", fs.constants.F_OK)
    console.log("文件存在")
  } catch (err) {
    console.log("文件不存在")
  }
}

checkFile()
DANGER

access() 方法不仅可以检测文件是否存在,也可以检测文件夹是否存在

文件读取 readFile

fs 模块为读取文件提供了两个方法,即 readFile() 方法和 readFileSync() 方法,二者的区别是,前者为异步读取文件(默认操作),后者为同步读取文件,这两个方法的语法格式如下:

  • file:文件名
  • encoding:文件的编码格式
  • callback:回调函数
javascript
fs.readFile(file, encoding, callback)
fs.readFileSync(file, encoding)

读取文件

javascript
const fs = require("fs")

// 使用 readFileSync() 方法同步读取文件
const text = fs.readFileSync("poems.txt", "utf8")
console.log(text)

// 使用 readFile() 方法异步读取文件
fs.readFile("demo.txt", "utf8", (err, data) => {
  if (err) {
    console.error("读取文件失败:", err)
    return
  }
  // 读取结果存储在回调函数的第 2 个参数 data 中
  console.log(data)
})

// 使用 Promise 风格
const fsPromises = require("fs").promises
fsPromises
  .readFile("demo.txt", "utf8")
  .then((data) => console.log(data))
  .catch((err) => console.error("读取文件失败:", err))

// 或使用 async/await
async function readFile() {
  try {
    const data = await fsPromises.readFile("demo.txt", "utf8")
    console.log(data)
  } catch (err) {
    console.error("读取文件失败:", err)
  }
}

readFile 方法的完整语法

javascript
fs.readFile(path[, options], callback)
fs.readFileSync(path[, options])

参数说明

  • path:文件路径(字符串、Buffer 或 URL)
  • options:可选参数,可以是:
    • encoding:编码格式,如 "utf8""ascii""base64" 等,默认值为 null(返回 Buffer)
    • flag:文件系统标志,默认值为 "r"(只读)
    • signal:允许中止正在进行的读取操作
  • callback:回调函数,参数为 (err, data)

不指定编码时返回 Buffer

javascript
const fs = require("fs")

fs.readFile("demo.txt", (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  // data 是 Buffer 对象
  console.log(data) // <Buffer 48 65 6c 6c 6f ...>
  console.log(data.toString("utf8")) // 转换为字符串
})

示例:模拟听歌时的显示歌词效果。代码中 for 循环中所有的内容都放在了匿名函数中,并且该匿名函数需要自动执行,这样在执行该文件时,保证了每次循环都会输出一句歌词

javascript
var fs = require("fs")

fs.readFile("./song.txt", function (err, data) {
  if (err) {
    return console.log("歌词文件读取失败")
  }
  data = data.toString()
  var lines = data.split("\n")
  // 遍历所有行,通过正则表达式匹配对应时间点(时间点的格式为[00:00.00]),并输出对应的歌词
  var reg = /\[(\d{2})\:(\d{2})\.(\d{2})\]\s*(.+)/
  for (var i = 0; i < lines.length; i++) {
    ;(function (index) {
      var line = lines[index]
      var matches = reg.exec(line)
      if (matches) {
        var m = parseFloat(matches[1]) // 获取分
        var s = parseFloat(matches[2]) // 获取秒
        var ms = parseFloat(matches[3]) // 获取毫秒
        // 获取定时器中要输出的内容
        var content = matches[4]
        var time = m * 60 * 1000 + s * 1000 + ms // 将分+秒+毫秒转换为毫秒
        //使用定时器,让每行内容在指定的时间输出
        setTimeout(function () {
          console.log(content)
        }, time)
      }
    })(i)
  }
})

下面是 song.txt 文件内容

javascript
[ti:hello]
[ar:张鑫]
[t_time:(03:46)]
[00:00.00] hello - 张鑫
[00:02.00] 词:王人
[00:04.00] 曲:王人
[00:06.00] 编曲:梁桦
[00:08.00] 歌词编辑:东东
[00:18.23] 每件事情都有发生的理由
[00:21.72] 可无法解释 遇见你
[00:26.15] 再多的心理准备 都抵抗不了
[00:29.89] 被现实打败的爱情
[00:34.27] 都习惯了同一个温度
[00:38.08] 你说这叫幸福
[00:42.32] 同时也忽略了一种残酷
[00:45.39] 我觉得 好无助
[00:50.69] 我想 我想
[00:53.54] 我想一起越过所有困难和阻挡
[00:57.64] 而 却不一样
[01:01.58] 虽然都有共同的理想
[01:06.10] 窗外 有阳光
[01:09.76] 透过了一丝缝隙照亮了一点希望
[01:14.07] 而晚上 的月亮
[01:17.94] 让再次陷入了彷徨
[01:25.66] 你问为什么喜欢拍照记录
[01:29.27] 答案我却说不出
[01:33.17] 怕如果走了不同方向
[01:37.06] 有照片让我回顾
[01:41.36] 回忆去过的每一个地方
[01:45.41] 和时而停顿的脚步
[01:49.43] 就这么停停顿顿一步接一步
[01:53.75] 直到没有路
[01:57.91] 我想 我想
[02:00.86] 我想一起越过所有困难和阻挡
[02:04.95] 而 却不一样
[02:08.74] 虽然都有共同的理想
[02:13.15] 窗外 有阳光
[02:16.83] 透过了一丝缝隙照亮了一点希望
[02:21.01] 而晚上 的月亮
[02:25.30] 让再次陷入了彷徨
[02:30.44] 如果每一个清晨
[02:33.82] 在你的温度里苏醒
[02:37.82] 闭眼聆听 有节奏的呼吸
[02:41.63] 哪怕只是一瞬间
[02:43.68] 哪怕只是一场梦
[02:50.74] 我想 我想
[02:53.60] 我想一起越过所有困难和阻挡
[02:57.73] 而 却不一样
[03:01.43] 虽然都有共同的理想
[03:06.16] 窗外 有阳光
[03:09.81] 透过了一丝缝隙照亮了一点希望
[03:13.72] 而晚上 的月亮
[03:18.04] 让再次陷入了彷徨
[03:23.35] 每件事情都有发生的理由
[03:26.88] 可无法解释 遇见你
[03:31.17] 再多的心理准备 都抵抗不了
[03:35.11] 被命运安排的相遇

文件写入

文件写入时,有 4 个方法供选择,分别为 writeFile() 方法、writeFileSync() 方法、appendFile() 方法和 appendFileSync() 方法,下面分别进行介绍

writeFile

这两个方法分别用来对文件进行异步和同步写入,它们的语法格式如下:

javascript
fs.writeFile(file, data[, options], callback)
fs.writeFileSync(file, data[, options])

参数说明

  • file:文件名或文件描述符
  • data:写入文件的内容,可以是字符串、Buffer 或 Uint8Array
  • options:可选参数
    • encoding:编码方式,默认值为 "utf8",如果 data 为缓冲区,则忽略 encoding 参数
    • mode:文件的权限模式,默认值为 0o666(可读可写)
    • flag:文件系统标志,默认值为 "w"(覆盖写入)
    • signal:允许中止正在进行的写入文件操作
  • callback:回调函数,参数为 (err)

文件系统标志(flag)完整对照表

标志说明文件不存在时数据流位置
r以只读方式打开文件抛出错误(ENOENT)文件起始处
r+以读写方式打开文件抛出错误(ENOENT)文件起始处
w以写入方式打开文件,文件存在则清零创建新文件文件起始处
w+以读写方式打开文件,文件存在则清零创建新文件文件起始处
a以追加方式打开文件,写入数据追加到文件末尾创建新文件文件结尾处
a+以读取和追加方式打开文件,写入数据追加到文件末尾创建新文件文件结尾处
x排他性创建文件(文件不存在时创建,存在时失败)创建新文件文件起始处
x+排他性创建并打开文件进行读写(存在时失败)创建新文件文件起始处
ax排他性追加创建(类似 a,但文件存在时失败)创建新文件文件结尾处
ax+排他性追加读写创建(类似 a+,但文件存在时失败)创建新文件文件结尾处
WARNING

rr+ 模式下,如果文件不存在会抛出 ENOENT 错误;而 ww+aa+ 模式下,文件不存在时会自动创建。x 系列标志用于确保不会意外覆盖已有文件。

示例:创建文件并且向文件中写入内容

javascript
const fs = require("fs")

const data =
  "       春夜喜雨\n\t\t杜甫\n好雨知时节,当春乃发生。\n随风潜入夜,润物细无声。\n野径云俱黑,江船火独明。\n晓看红湿处,花重锦官城。"

// 异步写入
fs.writeFile("poems.txt", data, "utf8", (err) => {
  if (err) {
    console.error("写入文件失败:", err)
    return
  }
  console.log("异步写入文件完成")
})

// 同步写入
try {
  fs.writeFileSync("newpoems.txt", data, "utf8")
  console.log("同步写入文件完成!")
} catch (err) {
  console.error("写入文件失败:", err)
}

// Promise 风格
const fsPromises = require("fs").promises
fsPromises
  .writeFile("poems.txt", data, "utf8")
  .then(() => console.log("写入文件完成"))
  .catch((err) => console.error("写入文件失败:", err))

写入 Buffer 数据

javascript
const fs = require("fs")

const buffer = Buffer.from("Hello Node.js", "utf8")
fs.writeFile("buffer.txt", buffer, (err) => {
  if (err) {
    console.error(err)
    return
  }
  console.log("Buffer 写入完成")
})

appendFile

这两个方法分别向文件异步追加内容和同步追加内容,它们的语法格式如下:

javascript
fs.appendFile(path, data[, options], callback)
fs.appendFileSync(path, data[, options])

参数说明

  • path:文件路径
  • data:要写入文件的数据
  • options:可选参数
    • encoding:编码方式,默认值为 "utf8"
    • mode:文件模式,默认值为 0o666
    • flag:文件系统标志,默认值为 "a"(追加模式)
  • callback:回调函数,参数为 (err)

示例:

javascript
const fs = require("fs")

const path = "poems.txt"
const data =
  "\n古诗鉴赏:这首诗描写细腻、动人。诗的情节从概括的叙述到形象的描绘,由耳闻到目睹,当晚到次晨,结构谨严。用词讲究。颇为难写的夜雨景色,却写得十分耀眼突出,使人从字里行间呼吸到一股令人喜悦的春天气息。"

// 异步追加
fs.appendFile(path, data, (err) => {
  if (err) {
    console.error("追加内容失败:", err)
    return
  }
  console.log("内容追加完成")
})

// Promise 风格
const fsPromises = require("fs").promises
fsPromises
  .appendFile(path, data)
  .then(() => console.log("内容追加完成"))
  .catch((err) => console.error("追加内容失败:", err))

文件操作时的异常处理

在实际编程中,经常会出现一些异常情况

1. 同步操作的异常处理

同步方法使用 try-catch 进行异常处理:

javascript
const fs = require("fs")

// 文件读取
try {
  const data = fs.readFileSync("textfile.txt", "utf8")
  console.log(data)
} catch (err) {
  console.error("读取文件失败:", err)
}

// 文件写入
try {
  fs.writeFileSync("textfile.txt", "Hello World!", "utf8")
  console.log("完成文件写入操作")
} catch (err) {
  console.error("写入文件失败:", err)
}

2. 异步操作的异常处理

使用回调函数风格的异步方法时,错误作为第一个参数传递:

javascript
const fs = require("fs")

// 文件读取
fs.readFile("textfile.txt", "utf8", (err, data) => {
  if (err) {
    console.error("读取文件失败:", err)
    return
  }
  console.log(data)
})

// 文件写入
fs.writeFile("textfile.txt", "Hello World!", "utf8", (err) => {
  if (err) {
    console.error("写入文件失败:", err)
    return
  }
  console.log("完成文件写入操作")
})

3. Promise 风格的异常处理

使用 Promise 风格时,可以使用 .catch()try-catch(配合 async/await):

javascript
const fsPromises = require("fs").promises

// 使用 .catch()
fsPromises
  .readFile("textfile.txt", "utf8")
  .then((data) => console.log(data))
  .catch((err) => console.error("读取文件失败:", err))

// 使用 async/await
async function readFile() {
  try {
    const data = await fsPromises.readFile("textfile.txt", "utf8")
    console.log(data)
  } catch (err) {
    console.error("读取文件失败:", err)
  }
}

4. 常见错误代码

错误代码说明
ENOENT文件或目录不存在
EACCES权限不足
EPERM操作不允许
EEXIST文件或目录已存在
ENOTDIR路径不是目录
EISDIR期望文件但得到目录
EMFILE打开的文件太多
ENOSPC磁盘空间不足

错误处理示例

javascript
const fs = require("fs")

fs.readFile("nonexistent.txt", "utf8", (err, data) => {
  if (err) {
    if (err.code === "ENOENT") {
      console.error("文件不存在")
    } else if (err.code === "EACCES") {
      console.error("权限不足")
    } else {
      console.error("其他错误:", err.message)
    }
    return
  }
  console.log(data)
})

文件操作

在 fs 模块中还提供了很多其他的文件操作方法,这些方法同样有同步方法和异步方法之分

截断文件 truncate

在 fs 模块中,可以使用 truncate() 方法对源文件进行截断操作,所谓截断,是指删除文件内的一部分内容,以改变文件的大小。其语法格式如下:

  • path:用于指定要被截断文件的完整文件路径及文件名
  • len:一个整数数值,用于指定被截断后的文件大小(以字节为单位)
  • callback:用于指定截断文件操作完毕时执行的回调函数,该回调函数中使用一个参数,参数值为截断文件操作失败时触发的错误对象
javascript
fs.truncate(path[, len], callback)
DANGER

注意当 len 为 0 时,说明文件的内容为空

示例:

javascript
var fs = require("fs")
fs.stat("poems.txt", function (err, stats) {
  console.log("原文件大小为:" + stats.size + "字节")
})
fs.truncate("poems.txt", 90, function (err) {
  if (err) console.log("对文件进行截断操作失败。")
  else {
    fs.stat("poems.txt", function (err, stats) {
      console.log("截断操作已完成\n文件大小为:" + stats.size + "字节。")
    })
  }
})

在 fs 模块中,可以使用 unlink() 方法对文件进行删除操作,其语法格式如下:

  • path:用于指定被删除文件的路径
  • callback:回调函数
javascript
fs.unlink(path, callback)

示例:删除指定被路径下的文本文件

javascript
var fs = require("fs")
console.log("准备删除文件!")

fs.unlink("poems.txt", function (err) {
  if (err) {
    console.error(err)
  }
  console.log("文件删除成功!")
})

复制文件

文件复制有两种形式:

  • 一种是将文件从一个位置复制到另外一个位置
  • 另一种则是从原文件中读取数据并写入一个新文件中

copyFile

copyFile() 方法与 copyFileSync() 方法分别用于异步和同步复制文件,它们用来将文件从一个位置复制到另外一个位置,其语法格式如下:

  • src:要复制的源文件名
  • dest:要复制的目标文件名
  • mode:复制操作的修饰符,默认值为 0
  • callback:回调函数
javascript
fs.copyFile(src, dest[, mode], callback)
fs.copyFileSync(src, dest[, mode])

示例:

javascript
var fs = require("fs")
fs.copyFile("poems.txt", "poems1.txt", function (err) {
  if (err) {
    console.log("复制文件失败")
    console.log(err)
  } else {
    console.log("复制文件成功")
  }
})

readFilewriteFile

除了上面介绍的直接复制文件的方式,还可以通过复制文件内容的方式实现文件的复制,这需要使用 fs 模块中的 readFile() 方法和 writeFile() 方法

javascript
var fs = require("fs")

fs.readFile("poems.txt", "utf8", function (err, data) {
  if (err) {
    console.log("读取文件失败")
    console.log(err)
  } else {
    console.log("读取文件成功")
    //写入文件
    fs.writeFile("poems1.txt", data, function (err) {
      if (err) {
        console.log("写入文件失败")
        console.log(err)
      } else {
        console.log("写入文件成功")
      }
    })
  }
})

重命名文件

fs 模块中,可以使用 rename() 方法为文件重命名,重命名文件的同时会更改文件的路径,其语法格式如下:

  • oldPath:原文件名(目录名)
  • newPath:新文件名(目录名)
  • callback:回调函数
javascript
fs.rename(oldPath, newPath, callback)

使用示例:

javascript
fs = require("fs")
fs.rename("poems.txt", "春夜喜雨.txt", function (err) {
  if (err) {
    console.log("糟糕!重命名文件失败")
    console.log(err)
  } else {
    console.log("重命名文件成功")
  }
})

示例:批量重命名文件

javascript
fs = require("fs")

//读取 demo 文件夹中的文件名
fs.readdir("demo", (err, files) => {
  for (var i = 0; i < files.length; i++) {
    fl = "demo\\" + files[i] //记录要重命名的文件名(包括路径)
    //读取文件,并将文件中的内容以换行符进行分割存储到数组中
    var data = fs.readFileSync(fl, "utf8").split("\n")
    // 获取文件中第一行内容并去掉所有空白字符(包括空格和换行符等)
    var title = data[0].replace(/\s*/g, "")
    fs.rename(fl, "demo\\" + title + ".txt", function (err) {
      if (err) console.error(err.message)
    })
  }
})

目录操作

除了对文件进行操作的方法,fs 模块还提供了一系列对目录(文件夹)进行操作的方法

创建目录 mkdir

创建目录可以使用 mkdir()mkdirSync() 方法实现,它们分别用来异步和同步创建目录,语法格式如下:

  • path:要被创建的目录的完整路径及目录名
  • options:指定目录的权限,默认为 0777(表示任何人可读可写该目录)
  • callback:指定创建目录操作完毕时调用的回调函数,该回调函数中只有一个参数,参数值为创建目录操作失败时触发的错误对象
javascript
fs.mkdir(path[, options], callback)
fs.mkdirSync(path[, options])

示例:批量创建文件并放到指定的文件夹中

javascript
var fs = require("fs")
//检查demo文件夹是否存在
fs.access("demo", fs.constants.F_OK, function (err) {
  if (err) {
    //如果不存在,则创建demo文件夹,反之则继续下一步
    fs.mkdir("demo", function (err) {
      if (err) console.log("糟糕,创建文件夹时出错了")
    })
  }
  //读取b.txt文件,该文件中保存了要批量创建的文件的名称
  var data = fs.readFileSync("b.txt", "utf8").split("\r")
  //逐个创建文件
  for (var i = 0; i < data.length; i++) {
    var title = data[i].replace("\n", "") //去掉读取到的内容中的换行符
    fs.writeFile("demo/" + title + ".txt", "", function (err) {
      if (err) console.log("创建文件失败")
    })
  }
})
DANGER

注意的是,创建目录时需要一级一级地创建,而不是直接创建多级目录

读取目录 readdir

fs 模块中,可以使用 readdir() 方法或者 readdirSync() 方法读取目录

  • path:文件名或者文件描述符
  • options:可选参数,可以为如下值
    • encoding:编码方式
    • flag:文件系统标志
    • signal:允许中止正在进行的读取文件操作
  • callback:回调函数,在回调函数中有两个参数
    • err:出现错误时的错误信息
    • data:调用成功时的返回值
javascript
fs.readdir(path[, options], callback)

示例:

javascript
const fs = require("fs")
fs.readdir("../test", function (err, files) {
  if (err) console.log("读取文件夹操作失败。")
  else console.log(files)
})

删除空目录 rmdir

fs 模块中,可以使用 rmdir() 方法或者 rmdirSync() 方法删除空目录,语法格式如下:

  • path:用于指定要被删除目录的完整路径以及目录名
  • options:可选参数,可以为如下
    • recursive:如果为 true,则执行递归目录删除操作。在递归模式下,操作将在失败时重试。默认值为 false
    • retryDelay:重试之间等待的时间(以毫秒为单位)。如果 recursive 选项不为 true,则忽略此选项。默认值为 100
    • maxRetries:表示重试次数,如果遇到 EBUSYEMFILEENFILEENOTEMPTYEPERM 错误,Node.js 将在每次尝试时,以 retryDelay 毫秒的线性退避等待时间重试该操作。如果 recursive 选项不为 true,则忽略此选项。默认值为 0
  • callback:用于指定删除目录操作完毕时调用的回调函数
javascript
fs.rmdir(path[, options], callback)

如果目录不为空,fs.rmdir 默认会抛出错误。可以设置选项 { recursive: true } 来允许递归删除非空目录

以下是 fs.rmdir 的使用示例:

1. 基本用法

javascript
const fs = require("fs")

// 指定要删除的目录路径
const path = "./dir-to-be-deleted"

// 调用 rmdir 函数
fs.rmdir(path, (err) => {
  if (err) {
    console.error("Error:", err)
    return
  }
  console.log("Directory deleted:", path)
})

2. 使用 Promises

如果更喜欢使用 Promises 而不是回调,你可以使用 fs.promises.rmdir 方法:

javascript
const fs = require("fs").promises

async function deleteDirectory() {
  try {
    await fs.rmdir("./dir-to-be-deleted")
    console.log("Directory deleted")
  } catch (err) {
    console.error("Error:", err)
  }
}

deleteDirectory()

3. 递归删除

fs.rmdir 默认就是递归删除,意味着它会删除目录及其所有内容。如果只想删除空目录,可以使用 fs.rmdirSync 的同步版本,或者在异步版本中检查目录是否为空:

javascript
const fs = require("fs")

fs.rmdir("./dir-to-be-deleted", { recursive: false }, (err) => {
  if (err) {
    if (err.code === "ENOTEMPTY") {
      console.error("Directory is not empty")
    } else {
      console.error("Error:", err)
    }
    return
  }
  console.log("Directory deleted:", "./dir-to-be-deleted")
})

查看目录信息

fs 模块中,可以使用 stat() 方法或者 lstat() 方法查看目录或文件信息,但如果是查看链接文件的信息,就必须使用 lstat() 方法。stat() 方法或者 lstat() 方法的语法格式如下:、

javascript
stat(path, callback)
lstat(path, callback)

示例:

javascript
const fs = require("fs")

fs.stat("demo", function (err, stats) {
  if (err) {
    console.log("获取文件夹信息失败")
  } else {
    console.log(stats)
  }
})

从返回的结果中可以看到很多属性信息

属性名称说明
dev表示文件或目录所在的设备 ID
mode表示文件或目录的权限
nlink表示文件或目录的硬连接数量
uid表示文件或目录的所有者的用户 ID
gid表示文件或目录所属组的数字标识
rdev表示字符设备文件或块设备文件所在的设备 ID
blksize表示文件或目录中 I/O 操作的块大小(以字节为单位)
ino表示文件或目录的索引编号
size表示文件或目录的大小(即文件中的字节数)
blocks表示分配给文件或目录的块数
atimeMs表示最后一次访问文件或目录时的时间戳(以毫秒为单位)
mtimeMs表示最后一次修改文件或目录时的时间戳(以毫秒为单位)
ctimcMS表示最后一次更改文件或目录状态时的时间戳(以毫秒为单位)
birthtimeMs表示创建文件或目录时的时间戳(以毫秒为单位)
atime表示上次访问文件或目录的时间戳
mtime表示上次修改文件或目录时的时间戳
ctime表示上次更改文件或目录状态时的时间戳
birthtime表示文件或目录创建时的时间戳

stat 对象的相关方法

方法说明
isFile判断是否是文件
isDirectory判断是否是目录(文件夹)
isBlockDevice判断是否是块设备
isCharacterDevice判断是否是字符设备
isFIFO判断是否是 FIFO 存储器
isSocket判断是否是 socket 协议

示例:

javascript
const fs = require("fs")
fs.stat("b.txt", function (err, stats) {
  console.log("是否为文件", stats.isFile())
  console.log("是否为文件夹/目录", stats.isDirectory())
})

获取目录的绝对路径 realpath

fs 模块中,可以使用 realpath() 方法或 realpathSync() 方法获取指定目录的绝对路径,其语法格式如下:

  • path:路径,可以为字符串或者 url
  • options:一般为 encoding,用于指定编码格式,默认为 utf8
  • callback:回调函数,该回调函数中有两个参数
    • err:发生错误时的错误信息
    • resolvedPath:绝对路径
javascript
fs.realpath(path[, options], callback)

示例:

javascript
const fs = require("fs")

fs.realpath("b.txt", (err, resolvedPath) => {
  if (err) {
    console.log("获取绝对路径失败")
    console.log(err)
  } else {
    console.log(resolvedPath)
  }
})

文件描述符操作

文件描述符(File Descriptor)是操作系统分配给打开文件的整数标识符。Node.js 提供了基于文件描述符的底层 API,适合需要精细控制文件操作的场景。

打开文件 open

javascript
fs.open(path[, flags[, mode]], callback)
fs.openSync(path[, flags[, mode]])
fsPromises.open(path[, flags[, mode]])

参数说明

  • path:文件路径
  • flags:文件打开标志,常见值:
    • "r":以只读方式打开
    • "r+":以读写方式打开
    • "w":以写入方式打开,文件不存在则创建
    • "w+":以读写方式打开,文件不存在则创建
    • "a":以追加方式打开,文件不存在则创建
    • "a+":以读取和追加方式打开
  • mode:文件权限(创建文件时),默认为 0o666
  • callback:回调函数,参数为 (err, fd)

使用示例

javascript
const fs = require("fs")

// 回调风格
fs.open("example.txt", "r", (err, fd) => {
  if (err) {
    console.error("打开文件失败:", err)
    return
  }
  console.log("文件描述符:", fd)
  // 使用完毕后关闭文件
  fs.close(fd, (err) => {
    if (err) console.error("关闭文件失败:", err)
  })
})

// Promise 风格
const fsPromises = require("fs").promises

async function openFile() {
  let fd
  try {
    fd = await fsPromises.open("example.txt", "r")
    console.log("文件描述符:", fd.fd)
    // 执行文件操作
  } catch (err) {
    console.error("操作失败:", err)
  } finally {
    if (fd) await fd.close()
  }
}

从文件描述符读取 read

javascript
fs.read(fd, buffer, offset, length, position, callback)
fs.readSync(fd, buffer, offset, length, position)

参数说明

  • fd:文件描述符,通过 fs.open() 获取
  • buffer:数据写入的缓冲区(Buffer 对象)
  • offset:缓冲区开始写入的位置(偏移量)
  • length:要读取的字节数
  • position:文件开始读取的位置(null 表示当前位置)
  • callback:回调函数,参数为 (err, bytesRead, buffer)

使用示例

javascript
const fs = require("fs")

fs.open("example.txt", "r", (err, fd) => {
  if (err) throw err

  const buffer = Buffer.alloc(1024)

  fs.read(fd, buffer, 0, buffer.length, 0, (err, bytesRead, buffer) => {
    if (err) throw err

    console.log(`读取了 ${bytesRead} 字节`)
    console.log(buffer.toString("utf8", 0, bytesRead))

    fs.close(fd, (err) => {
      if (err) throw err
    })
  })
})

精细化读取示例:从文件指定位置读取指定长度的数据

javascript
const fs = require("fs")

fs.open("./a.txt", "r", function (err, fd) {
  if (err) throw err

  const buf = Buffer.alloc(1024)
  const offset = 0       // 写入 buffer 的起始位置
  const len = buf.length // 写入 buffer 的长度
  const pos = 101        // 从文件第 101 个字节开始读取

  fs.read(fd, buf, offset, len, pos, function (err, bytes, buffer) {
    if (err) throw err
    console.log("读取了 " + bytes + " bytes")
    console.log(buf.slice(0, bytes).toString())
    fs.close(fd, function (err) {})
  })
})

实际应用:判断文件是否为 PNG 格式

PNG 文件头部 8 字节是固定的标识数据,可以通过读取前 8 字节来判断文件是否为 PNG 格式:

javascript
const fs = require("fs")

fs.open("image.png", "r", function (err, fd) {
  if (err) throw err

  const header = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]) // PNG 标识
  const buf = Buffer.alloc(8)

  fs.read(fd, buf, 0, buf.length, 0, function (err, bytes, buffer) {
    if (err) throw err

    if (header.toString() === buffer.toString()) {
      console.log("是 PNG 图片")
    } else {
      console.log("不是 PNG 图片")
    }
    fs.close(fd, function (err) {})
  })
})

写入到文件描述符 write

javascript
fs.write(fd, buffer[, offset[, length[, position]]], callback)
fs.write(fd, string[, position[, encoding]], callback)
fs.writeSync(fd, buffer, offset, length, position)

参数说明

  • fd:文件描述符,通过 fs.open() 获取
  • buffer:要写入的 Buffer 数据(Buffer 尺寸建议设为 8 的倍数,效率更高)
  • offset:Buffer 写入的偏移量,一般从 0 开始,每写一次修改偏移量保证数据连续性
  • length:要写入的 Buffer 字节数长度
  • position:写入到文件的什么位置,null 表示从当前文件指针位置写入
  • callback:回调函数,参数为 (err, written, buffer),其中 written 是写入的字节数

使用示例(写入 Buffer 数据)

javascript
const fs = require("fs")

fs.open("output.txt", "w", (err, fd) => {
  if (err) throw err

  const data = "Hello, Node.js!"
  const buffer = Buffer.from(data, "utf8")

  fs.write(fd, buffer, 0, buffer.length, 0, (err, written, buffer) => {
    if (err) throw err

    console.log(`写入了 ${written} 字节`)

    fs.close(fd, (err) => {
      if (err) throw err
    })
  })
})

精细化写入示例:向文件指定位置写入数据

javascript
const fs = require("fs")

fs.open("./c.txt", "a", function (err, fd) {
  if (err) throw err

  const buf = Buffer.from("I Love Node.js")
  const offset = 0
  const len = buf.length
  const pos = 100  // 从文件第 100 个字节位置开始写入

  fs.write(fd, buf, offset, len, pos, function (err, bytes, buffer) {
    if (err) throw err
    console.log("写入了 " + bytes + " bytes")
    console.log(buf.slice(0, bytes).toString())
    fs.close(fd, function (err) {})
  })
})

写入字符串数据

fs.write() 的第二种用法是直接写入字符串:

javascript
const fs = require("fs")

fs.open("./c.txt", "a", function (err, fd) {
  if (err) throw err

  const data = "Hello World"

  // 参数:文件描述符、写入的字符串、写入位置、编码格式、回调函数
  fs.write(fd, data, 0, "utf-8", function (err, written, string) {
    if (err) throw err
    console.log("写入了 " + written + " 字符")
    console.log(string)
    fs.close(fd, function (err) {})
  })
})
WARNING

多次对同一文件进行 write 操作并不安全,必须等待回调函数执行完成才能进行下一次写入。官方推荐使用 createWriteStream 流方式替代,详见 流操作(Stream) 章节。

关闭文件描述符 close

javascript
fs.close(fd, callback)
fs.closeSync(fd)
fsPromises.close(fd)

重要提示

DANGER

使用完文件描述符后,务必关闭文件,否则会导致资源泄漏。推荐使用 try-finallytry-with-resources 模式确保文件被关闭。

最佳实践

javascript
const fsPromises = require("fs").promises

async function processFile() {
  let fileHandle
  try {
    fileHandle = await fsPromises.open("example.txt", "r+")
    const buffer = Buffer.alloc(1024)
    await fileHandle.read(buffer, 0, buffer.length, 0)
    // 处理数据
  } catch (err) {
    console.error("操作失败:", err)
  } finally {
    if (fileHandle) {
      await fileHandle.close()
    }
  }
}

粗粒度 vs 精细化读写操作对比

fs 模块的读写操作分为两种模式,适用于不同场景:

对比维度粗粒度操作精细化操作
方法readFile/writeFile/appendFileopen + read/write + close
操作次数一次性完成可多次读写
文件关闭内部自动调用 close需手动调用 close
位置控制不支持指定读写位置支持通过 position 参数精确控制
适用场景简单的文件读写、小文件需要多次读写、指定位置读写、大文件分块处理

选择建议

  • 如果只需一次性读取或写入整个文件,使用 readFile/writeFile/appendFile 即可,它们内部会自动管理文件关闭
  • 如果需要多次对同一文件进行读写操作,或需要精确控制读写位置,应使用 open + read/write + close 的精细化方式
  • 对于大文件或高频写入场景,推荐使用 Stream 方式(createReadStream/createWriteStream),详见 流操作(Stream) 章节

文件权限操作

修改文件权限 chmod

javascript
fs.chmod(path, mode, callback)
fs.chmodSync(path, mode)
fsPromises.chmod(path, mode)

参数说明

  • path:文件路径
  • mode:权限模式,可以是八进制数(如 0o755)或字符串(如 "755"

权限说明

权限数值说明
r (read)4读取权限
w (write)2写入权限
x (execute)1执行权限

常用权限组合

  • 0o755 (rwxr-xr-x):所有者可读写执行,其他用户可读执行
  • 0o644 (rw-r--r--):所有者可读写,其他用户只读
  • 0o600 (rw-------):仅所有者可读写
  • 0o777 (rwxrwxrwx):所有用户都有完整权限(不推荐)

使用示例

javascript
const fs = require("fs")

// 回调风格
fs.chmod("script.sh", 0o755, (err) => {
  if (err) {
    console.error("修改权限失败:", err)
    return
  }
  console.log("权限修改成功")
})

// Promise 风格
const fsPromises = require("fs").promises

async function setPermissions() {
  try {
    await fsPromises.chmod("config.json", 0o600)
    console.log("配置文件权限已设置为仅所有者可读写")
  } catch (err) {
    console.error("修改权限失败:", err)
  }
}

修改文件所有者 chown

javascript
fs.chown(path, uid, gid, callback)
fs.chownSync(path, uid, gid)
fsPromises.chown(path, uid, gid)

参数说明

  • path:文件路径
  • uid:用户 ID(User ID)
  • gid:组 ID(Group ID)

使用示例

javascript
const fs = require("fs")

// 将文件所有者改为用户 ID 为 1000,组 ID 为 1000
fs.chown("file.txt", 1000, 1000, (err) => {
  if (err) {
    console.error("修改所有者失败:", err)
    return
  }
  console.log("所有者修改成功")
})
WARNING

修改文件所有者通常需要 root 权限或文件所有者权限。在 Windows 系统上,此方法可能无效或表现不同。

获取文件权限信息

通过 stat() 方法可以获取文件的权限信息:

javascript
const fs = require("fs")

fs.stat("file.txt", (err, stats) => {
  if (err) throw err

  // 获取权限模式(八进制)
  const mode = stats.mode & 0o777
  console.log(`权限模式: ${mode.toString(8)}`)

  // 判断权限
  const isReadable = !!(stats.mode & fs.constants.S_IRUSR)
  const isWritable = !!(stats.mode & fs.constants.S_IWUSR)
  const isExecutable = !!(stats.mode & fs.constants.S_IXUSR)

  console.log(`可读: ${isReadable}, 可写: ${isWritable}, 可执行: ${isExecutable}`)
})

链接操作

硬链接是文件的另一个名称,指向相同的 inode。删除原文件不会影响硬链接。

javascript
fs.link(existingPath, newPath, callback)
fs.linkSync(existingPath, newPath)
fsPromises.link(existingPath, newPath)

使用示例

javascript
const fs = require("fs")

// 为 file.txt 创建硬链接 file-hardlink.txt
fs.link("file.txt", "file-hardlink.txt", (err) => {
  if (err) {
    console.error("创建硬链接失败:", err)
    return
  }
  console.log("硬链接创建成功")
})

// Promise 风格
const fsPromises = require("fs").promises

async function createHardLink() {
  try {
    await fsPromises.link("original.txt", "hardlink.txt")
    console.log("硬链接创建成功")
  } catch (err) {
    console.error("创建失败:", err)
  }
}

硬链接特点

  • 硬链接和原文件共享相同的 inode
  • 删除原文件不会删除硬链接
  • 硬链接不能跨文件系统
  • 硬链接不能指向目录(大多数系统)

符号链接(软链接)是特殊的文件,包含指向另一个文件的路径。

javascript
fs.symlink(target, path[, type], callback)
fs.symlinkSync(target, path[, type])
fsPromises.symlink(target, path[, type])

参数说明

  • target:链接指向的目标路径
  • path:符号链接的路径
  • type:链接类型(仅 Windows)
    • "file":文件链接
    • "dir":目录链接
    • "junction":Windows 目录连接点

使用示例

javascript
const fs = require("fs")

// 创建文件符号链接
fs.symlink("original.txt", "symlink.txt", (err) => {
  if (err) {
    console.error("创建符号链接失败:", err)
    return
  }
  console.log("符号链接创建成功")
})

// 创建目录符号链接(Windows)
fs.symlink("original-dir", "symlink-dir", "junction", (err) => {
  if (err) throw err
  console.log("目录符号链接创建成功")
})

// Promise 风格
const fsPromises = require("fs").promises

async function createSymlink() {
  try {
    // 文件链接
    await fsPromises.symlink("target.txt", "link.txt")
    console.log("文件符号链接创建成功")

    // 目录链接
    await fsPromises.symlink("target-dir", "link-dir", "junction")
    console.log("目录符号链接创建成功")
  } catch (err) {
    console.error("创建失败:", err)
  }
}

符号链接特点

  • 符号链接有自己的 inode
  • 删除原文件后,符号链接成为"悬空链接"
  • 可以跨文件系统
  • 可以指向目录
  • Windows 创建符号链接可能需要管理员权限
javascript
fs.readlink(path[, options], callback)
fs.readlinkSync(path[, options])
fsPromises.readlink(path[, options])

使用示例

javascript
const fs = require("fs")

fs.readlink("symlink.txt", (err, linkString) => {
  if (err) {
    console.error("读取符号链接失败:", err)
    return
  }
  console.log("符号链接指向:", linkString)
})

// Promise 风格
const fsPromises = require("fs").promises

async function readSymlink() {
  try {
    const target = await fsPromises.readlink("symlink.txt")
    console.log("符号链接指向:", target)
  } catch (err) {
    console.error("读取失败:", err)
  }
}

检查是否为符号链接

使用 lstat() 方法可以获取符号链接本身的信息(而不是目标文件的信息):

javascript
const fs = require("fs")

fs.lstat("symlink.txt", (err, stats) => {
  if (err) throw err

  if (stats.isSymbolicLink()) {
    console.log("这是一个符号链接")
  } else if (stats.isFile()) {
    console.log("这是一个普通文件")
  }
})

链接操作对比表

特性硬链接符号链接
是否有独立 inode否(共享)
原文件删除后仍可访问成为悬空链接
是否可跨文件系统
是否可指向目录通常不可以可以
占用空间几乎不占占少量空间
Windows 权限要求普通权限可能需要管理员权限

流操作(Stream)

对于大文件,使用 readFile()writeFile() 会将整个文件加载到内存中,可能导致内存溢出。使用流(Stream)可以分块处理文件,更加高效。

创建可读流 createReadStream

javascript
fs.createReadStream(path[, options])

常用选项

  • encoding:编码格式,默认值为 null(返回 Buffer)
  • flags:文件系统标志,默认值为 "r"
  • start:开始读取的位置(字节)
  • end:结束读取的位置(字节)
  • highWaterMark:缓冲区大小,默认值为 64KB

基本使用

javascript
const fs = require("fs")

// 创建可读流
const readStream = fs.createReadStream("large-file.txt", "utf8")

// 监听数据事件
readStream.on("data", (chunk) => {
  console.log("接收到数据块:", chunk.length, "字节")
  // 处理数据块
})

// 监听结束事件
readStream.on("end", () => {
  console.log("文件读取完成")
})

// 监听错误事件
readStream.on("error", (err) => {
  console.error("读取错误:", err)
})

创建可写流 createWriteStream

javascript
fs.createWriteStream(path[, options])

常用选项

  • encoding:编码格式,默认值为 "utf8"
  • flags:文件系统标志,默认值为 "w"
  • start:开始写入的位置(字节)
  • highWaterMark:缓冲区大小,默认值为 16KB

基本使用

javascript
const fs = require("fs")

// 创建可写流
const writeStream = fs.createWriteStream("output.txt", "utf8")

// 写入数据
writeStream.write("第一行数据\n")
writeStream.write("第二行数据\n")
writeStream.write("第三行数据\n")

// 结束写入
writeStream.end()

// 监听完成事件
writeStream.on("finish", () => {
  console.log("文件写入完成")
})

// 监听错误事件
writeStream.on("error", (err) => {
  console.error("写入错误:", err)
})

使用管道(pipe)复制文件

使用 pipe() 方法可以高效地复制大文件:

javascript
const fs = require("fs")

// 创建可读流和可写流
const readStream = fs.createReadStream("source.txt")
const writeStream = fs.createWriteStream("destination.txt")

// 使用管道连接
readStream.pipe(writeStream)

// 监听完成事件
writeStream.on("finish", () => {
  console.log("文件复制完成")
})

// 监听错误
readStream.on("error", (err) => {
  console.error("读取错误:", err)
})

writeStream.on("error", (err) => {
  console.error("写入错误:", err)
})

流操作示例

示例 1:逐行读取大文件

javascript
const fs = require("fs")
const readline = require("readline")

const readStream = fs.createReadStream("large-file.txt", "utf8")
const rl = readline.createInterface({
  input: readStream,
  crlfDelay: Infinity
})

rl.on("line", (line) => {
  console.log("读取到一行:", line)
  // 处理每一行
})

rl.on("close", () => {
  console.log("文件读取完成")
})

示例 2:流式写入数据

javascript
const fs = require("fs")

const writeStream = fs.createWriteStream("log.txt", { flags: "a" }) // 追加模式

// 模拟写入日志
for (let i = 0; i < 1000; i++) {
  writeStream.write(`日志条目 ${i}: ${new Date().toISOString()}\n`)
}

writeStream.end(() => {
  console.log("所有数据写入完成")
})

文件监控

监控文件变化 watchFile

watchFile() 方法可以监控文件的变化,当文件被修改时会触发回调:

javascript
fs.watchFile(filename[, options], listener)

参数说明

  • filename:要监控的文件路径
  • options:可选参数
    • interval:轮询间隔(毫秒),默认值为 5007
    • persistent:是否持续监控,默认值为 true
  • listener:回调函数,参数为 (currentStats, previousStats)

使用示例

javascript
const fs = require("fs")

fs.watchFile("config.json", { interval: 1000 }, (current, previous) => {
  console.log("文件被修改")
  console.log("当前大小:", current.size)
  console.log("之前大小:", previous.size)
  console.log("修改时间:", current.mtime)
})

// 停止监控
// fs.unwatchFile("config.json")

监控文件或目录 watch

watch() 方法使用底层文件系统事件,比 watchFile() 更高效:

javascript
fs.watch(filename[, options][, listener])

参数说明

  • filename:要监控的文件或目录路径
  • options:可选参数
    • recursive:是否递归监控子目录(仅限 macOS 和 Windows),默认值为 false
    • encoding:用于指定传递给监听器的文件名编码,默认值为 "utf8"
  • listener:回调函数,参数为 (eventType, filename)

事件类型

  • "rename":文件或目录被重命名或删除
  • "change":文件被修改

使用示例

javascript
const fs = require("fs")

const watcher = fs.watch(".", { recursive: true }, (eventType, filename) => {
  console.log(`事件类型: ${eventType}`)
  if (filename) {
    console.log(`文件名: ${filename}`)
  }
})

// 监听错误
watcher.on("error", (err) => {
  console.error("监控错误:", err)
})

// 关闭监控
// watcher.close()

实际应用示例:自动重启服务器

javascript
const fs = require("fs")
const { spawn } = require("child_process")

let serverProcess = null

function startServer() {
  if (serverProcess) {
    serverProcess.kill()
  }
  serverProcess = spawn("node", ["server.js"], { stdio: "inherit" })
  console.log("服务器已启动")
}

// 监控 server.js 文件
fs.watch("server.js", (eventType) => {
  if (eventType === "change") {
    console.log("检测到文件变化,重启服务器...")
    startServer()
  }
})

startServer()

实战案例

案例 1:MD 文件转换为 HTML

实现一个工具,将 Markdown 文件转换为 HTML,并监听文件变化自动更新。

功能需求

  1. 读取 md 和 css 内容
  2. 将读取的内容替换占位符,生成最终的 HTML 字符串
  3. 将 HTML 字符串写入到指定的 HTML 文件中
  4. 监听 md 文档内容的变化,然后更新 HTML 内容
  5. 使用 browser-sync 来实时显示 HTML 内容

实现代码

javascript
const fs = require("fs")
const path = require("path")
const marked = require("marked")
const browserSync = require("browser-sync")

const mdPath = path.resolve("./assets/TS4类型系统基础.md")
const cssPath = path.resolve("./assets/github.css")

// 替换文件的扩展名
const htmlPath = mdPath.replace(path.extname(mdPath), ".html")

// HTML 模板
const temp = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <style>
        .markdown-body {
            box-sizing: border-box;
            min-width: 200px;
            max-width: 1000px;
            margin: 0 auto;
            padding: 45px;
        }
        @media (max-width: 750px) {
            .markdown-body {
                padding: 15px;
            }
        }
        {{style}}
    </style>
</head>
<body>
    <div class="markdown-body">
        {{content}}
    </div>
</body>
</html>
`

// 生成 HTML 的函数
function generateHTML() {
  // 读取 Markdown 文件
  fs.readFile(mdPath, "utf8", (err, mdData) => {
    if (err) {
      console.error("读取 Markdown 文件失败:", err)
      return
    }

    // 将 md 转换为 html
    const htmlStr = marked.parse(mdData)

    // 读取 CSS 文件
    fs.readFile(cssPath, "utf8", (err, cssData) => {
      if (err) {
        console.error("读取 CSS 文件失败:", err)
        return
      }

      // 替换占位符
      const retHtml = temp.replace("{{content}}", htmlStr).replace("{{style}}", cssData)

      // 写入 HTML 文件
      fs.writeFile(htmlPath, retHtml, (err) => {
        if (err) {
          console.error("写入 HTML 文件失败:", err)
          return
        }
        console.log("HTML 成功生成")
      })
    })
  })
}

// 监听文件的变化
fs.watchFile(mdPath, (curr, prev) => {
  if (curr.mtime !== prev.mtime) {
    console.log("检测到文件变化,重新生成 HTML...")
    generateHTML()
  }
})

// 初始生成
generateHTML()

// 启动 browser-sync
browserSync({
  browser: "",
  server: path.dirname(mdPath),
  watch: true,
  index: path.basename(htmlPath)
})

案例 2:使用 Buffer 拷贝文件

利用 Buffer 缓冲区拷贝文件,适合大文件的分块处理。

实现思路

  • 打开源文件,利用 read 将数据保存到 buffer 暂存起来
  • 打开目标文件,利用 write 将 buffer 中数据写入到目标文件中
  • 循环读取和写入,直到文件拷贝完成

实现代码

javascript
const fs = require("fs")

const BUFFER_SIZE = 10 * 1024 // 10KB 缓冲区
const buf = Buffer.alloc(BUFFER_SIZE)
let readOffset = 0

fs.open("./assets/source.jpg", "r", (err, rfd) => {
  if (err) {
    console.error("打开源文件失败:", err)
    return
  }

  fs.open("./assets/destination.jpg", "w", (err, wfd) => {
    if (err) {
      console.error("打开目标文件失败:", err)
      fs.close(rfd, () => {})
      return
    }

    function next() {
      // 从源文件读取数据到 buffer
      fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes) => {
        if (err) {
          console.error("读取文件失败:", err)
          fs.close(rfd, () => {})
          fs.close(wfd, () => {})
          return
        }

        // 如果没有读取到数据,说明文件读取完成
        if (!readBytes) {
          fs.close(rfd, () => {
            console.log("源文件已关闭")
          })
          fs.close(wfd, () => {
            console.log("目标文件已关闭")
          })
          console.log("拷贝完成")
          return
        }

        // 更新读取偏移量
        readOffset += readBytes

        // 将 buffer 中的数据写入目标文件
        fs.write(wfd, buf, 0, readBytes, (err, written) => {
          if (err) {
            console.error("写入文件失败:", err)
            fs.close(rfd, () => {})
            fs.close(wfd, () => {})
            return
          }
          // 继续读取下一块
          next()
        })
      })
    }

    // 开始拷贝
    next()
  })
})

优化版本(使用流操作)

javascript
const fs = require("fs")

// 使用流操作更简单高效
const readStream = fs.createReadStream("./assets/source.jpg")
const writeStream = fs.createWriteStream("./assets/destination.jpg")

readStream.pipe(writeStream)

readStream.on("end", () => {
  console.log("拷贝完成")
})

readStream.on("error", (err) => {
  console.error("读取错误:", err)
})

writeStream.on("error", (err) => {
  console.error("写入错误:", err)
})

案例 3:递归创建目录

实现一个函数,可以递归创建多级目录

  1. 同步实现
javascript
const fs = require("fs")
const path = require("path")

/**
 * 同步创建多级目录
 * @param {string} dirPath - 要创建的目录路径,如 "a/b/c"
 */
function makeDirSync(dirPath) {
  // 使用 path.sep 确保跨平台兼容
  const dirs = dirPath.split(path.sep)

  // 遍历数组,逐级创建目录
  for (let i = 0; i < dirs.length; i++) {
    // 拼接当前路径
    const current = dirs.slice(0, i + 1).join(path.sep)

    // 判断当前路径是否存在
    if (!fs.existsSync(current)) {
      fs.mkdirSync(current)
      console.log(`创建目录: ${current}`)
    }
  }
}

// 使用示例
makeDirSync("a/b/c")
  1. 异步实现(回调函数风格)
javascript
const fs = require("fs")
const path = require("path")

/**
 * 异步创建多级目录
 * @param {string} dirpath - 要创建的目录路径
 * @param {Function} callback - 回调函数
 */
function mkdirs(dirpath, callback) {
  // 检查目录是否存在
  fs.access(dirpath, fs.constants.F_OK, (err) => {
    if (!err) {
      // 目录已存在
      callback()
    } else {
      // 目录不存在,先创建父目录
      mkdirs(path.dirname(dirpath), () => {
        // 父目录创建成功后,创建当前目录
        fs.mkdir(dirpath, callback)
      })
    }
  })
}

// 使用示例
mkdirs("a/b/c/d", () => {
  console.log("目录创建完成")
})
  1. Promise 风格实现
javascript
const fs = require("fs")
const path = require("path")
const { promisify } = require("util")

// 将 access 与 mkdir 转换为 Promise 风格
const access = promisify(fs.access)
const mkdir = promisify(fs.mkdir)

/**
 * Promise 风格创建多级目录
 * @param {string} dirPath - 要创建的目录路径
 * @param {Function} callback - 可选的回调函数
 */
async function myMkdir(dirPath, callback) {
  const parts = dirPath.split(path.sep)

  for (let index = 1; index <= parts.length; index++) {
    const current = parts.slice(0, index).join(path.sep)

    try {
      // 检查目录是否存在
      await access(current)
    } catch (err) {
      // 目录不存在,创建它
      await mkdir(current)
      console.log(`创建目录: ${current}`)
    }
  }

  // 如果提供了回调函数,执行它
  if (callback) {
    callback()
  }
}

// 使用示例
myMkdir("a/b/c", () => {
  console.log("创建成功")
})
  .then(() => {
    console.log("所有操作完成")
  })
  .catch((err) => {
    console.error("创建失败:", err)
  })

// 或使用 async/await
async function createDirs() {
  try {
    await myMkdir("x/y/z")
    console.log("目录创建完成")
  } catch (err) {
    console.error("创建失败:", err)
  }
}

案例 4:递归删除目录

实现一个函数,可以递归删除目录及其所有内容。

实现思路

  • 判断当前传入的路径是否为文件,是则直接删除
  • 如果当前传入的是一个目录,需要继续读取目录中的内容,然后再执行删除操作
  • 将删除行为定义成一个函数,通过递归的方式进行复用
  • 将当前的文件名拼接成在删除时可使用的路径

实现代码

javascript
const fs = require("fs")
const path = require("path")

/**
 * 递归删除目录或文件
 * @param {string} dirPath - 要删除的路径
 * @param {Function} callback - 回调函数
 */
function myRmdir(dirPath, callback) {
  // 判断当前 dirPath 的类型
  fs.stat(dirPath, (err, statObj) => {
    if (err) {
      callback(err)
      return
    }

    if (statObj.isDirectory()) {
      // 是目录,读取目录内容
      fs.readdir(dirPath, (err, files) => {
        if (err) {
          callback(err)
          return
        }

        // 将文件名拼接成完整路径
        const dirs = files.map((item) => {
          return path.join(dirPath, item)
        })

        let index = 0

        function next() {
          // 如果所有子项都已删除,删除当前目录
          if (index === dirs.length) {
            fs.rmdir(dirPath, callback)
            return
          }

          // 递归删除当前子项
          const current = dirs[index++]
          myRmdir(current, next)
        }

        // 如果目录为空,直接删除
        if (dirs.length === 0) {
          fs.rmdir(dirPath, callback)
        } else {
          next()
        }
      })
    } else {
      // 是文件,直接删除
      fs.unlink(dirPath, callback)
    }
  })
}

// 使用示例
myRmdir("tmp", (err) => {
  if (err) {
    console.error("删除失败:", err)
    return
  }
  console.log("删除成功")
})

Promise 风格实现

javascript
const fsPromises = require("fs").promises
const path = require("path")

/**
 * Promise 风格递归删除目录或文件
 * @param {string} dirPath - 要删除的路径
 */
async function myRmdirAsync(dirPath) {
  try {
    const statObj = await fsPromises.stat(dirPath)

    if (statObj.isDirectory()) {
      // 读取目录内容
      const files = await fsPromises.readdir(dirPath)

      // 递归删除所有子项
      await Promise.all(
        files.map((file) => {
          const filePath = path.join(dirPath, file)
          return myRmdirAsync(filePath)
        })
      )

      // 删除当前目录
      await fsPromises.rmdir(dirPath)
    } else {
      // 删除文件
      await fsPromises.unlink(dirPath)
    }
  } catch (err) {
    throw err
  }
}

// 使用示例
myRmdirAsync("tmp")
  .then(() => {
    console.log("删除成功")
  })
  .catch((err) => {
    console.error("删除失败:", err)
  })

案例 5:目录操作 API 综合示例

综合使用各种目录操作 API:

javascript
const fs = require("fs")

// access:判断文件或目录是否具有操作权限
fs.access("./data.txt", fs.constants.F_OK, (err) => {
  console.log(err ? "no access!" : "can read/write")
})

// stat:获取目录及文件信息
fs.stat("./data.txt", (err, stats) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(`文件属性:${JSON.stringify(stats, null, 2)}`)
  console.log(`是否是文件:${stats.isFile()}`)
  console.log(`是否是目录:${stats.isDirectory()}`)
  console.log(`文件大小:${stats.size} 字节`)
  console.log(`创建时间:${stats.birthtime}`)
  console.log(`修改时间:${stats.mtime}`)
})

// mkdir:创建目录(支持递归创建)
fs.mkdir("a/b/c", { recursive: true }, (err) => {
  if (err) {
    console.error("创建目录失败:", err)
    return
  }
  console.log("目录创建成功")
})

// readdir:读取目录内容
fs.readdir("./", (err, files) => {
  if (err) {
    console.error("读取目录失败:", err)
    return
  }
  console.log("目录内容:", files)
})

// 读取目录(包含文件类型信息)
fs.readdir("./", { withFileTypes: true }, (err, files) => {
  if (err) {
    console.error("读取目录失败:", err)
    return
  }
  files.forEach((file) => {
    if (file.isDirectory()) {
      console.log(`目录: ${file.name}`)
    } else {
      console.log(`文件: ${file.name}`)
    }
  })
})

// unlink:删除指定文件
fs.unlink("./file.jpg", (err) => {
  if (err) {
    console.error("删除文件失败:", err)
    return
  }
  console.log("文件删除成功")
})

// rmdir:删除指定目录(支持递归删除)
fs.rmdir("./a", { recursive: true }, (err) => {
  if (err) {
    console.error("删除目录失败:", err)
    return
  }
  console.log("目录删除成功")
})

案例 6:中英文 JSON 合并工具

在开发国际化网站时,通常需要处理 i18n(国际化)内容。当页面较多时,不便使用单一 JSON 文件囊括所有翻译内容,通常会按页面区分翻译文件,最终合并成一份完整的数据。

功能需求

假设有以下按页面区分的翻译文件:

pagea.json:

json
{
  "signup": "注册"
}

pageb.json:

json
{
  "menu": "菜单"
}

希望将它们合并成 data.json:

json
{
  "pagea": {
    "signup": "注册"
  },
  "pageb": {
    "menu": "菜单"
  }
}

实现代码

javascript
const fs = require("fs")
const path = require("path")

// 判断目标路径的文件存在与否
const exists = filePath => fs.existsSync(filePath)

// 从命令行参数获取 JSON 目录路径
const jsonPath = process.argv[2]

if (!jsonPath) {
  console.log("请传入 JSON 目录参数!用法: node merge.js <json目录路径>")
  process.exit(1)
}

const rootPath = path.join(process.cwd(), jsonPath)

// 遍历目录,收集所有 JSON 文件路径
const walk = (dirPath) => fs
  .readdirSync(dirPath)
  .reduce((files, file) => {
    const filePath = path.join(dirPath, file)
    const stat = fs.statSync(filePath)

    if (stat.isFile()) {
      if (/(.*)\.(json)/.test(file)) {
        return files.concat(filePath)
      }
    }
    return files
  }, [])

// 合并文件内容
const mergeFileData = () => {
  const files = walk(rootPath)

  if (!files.length) {
    console.log("未找到任何 JSON 文件")
    process.exit(2)
  }

  const data = files
    .filter(exists)
    .reduce((total, file) => {
      const fileData = fs.readFileSync(file, "utf8")
      const basename = path.basename(file, ".json")
      let fileJson

      try {
        fileJson = JSON.parse(fileData)
      } catch (err) {
        console.log("JSON 解析出错:", file)
        console.log(err)
        return total
      }

      // 以文件名(不含扩展名)作为 key,文件内容作为 value
      total[basename] = fileJson
      return total
    }, {})

  // 写入合并后的数据
  fs.writeFileSync("./data.json", JSON.stringify(data, null, 2))
  console.log(`合并完成!共处理 ${files.length} 个文件,输出到 data.json`)
}

mergeFileData()

使用方法

bash
# 假设翻译文件存放在 ./i18n/zh 目录下
node merge.js ./i18n/zh

# 合并结果将输出到当前目录的 data.json

功能扩展建议

  1. 支持递归遍历子目录:修改 walk 函数,支持递归读取子目录中的 JSON 文件
  2. 支持多语言合并:遍历多个语言目录,生成按语言区分的合并文件
  3. 添加文件监控:使用 fs.watch 监控文件变化,自动重新合并
  4. 支持增量更新:记录文件修改时间,只合并有变化的文件

递归遍历版本

javascript
const fs = require("fs")
const path = require("path")

// 递归遍历目录,收集所有 JSON 文件路径
const walk = (dirPath) => fs
  .readdirSync(dirPath)
  .reduce((files, file) => {
    const filePath = path.join(dirPath, file)
    const stat = fs.statSync(filePath)

    if (stat.isFile()) {
      if (/(.*)\.(json)/.test(file)) {
        return files.concat(filePath)
      }
    } else if (stat.isDirectory()) {
      // 递归处理子目录
      return files.concat(walk(filePath))
    }
    return files
  }, [])

// 使用示例
const allJsonFiles = walk("./i18n")
console.log(`找到 ${allJsonFiles.length} 个 JSON 文件`)

最佳实践

1. 优先使用异步方法

异步方法不会阻塞事件循环,适合生产环境:

javascript
// ✅ 推荐:异步方法
fs.readFile("file.txt", "utf8", (err, data) => {
  // 处理数据
})

// ❌ 不推荐:同步方法(会阻塞)
const data = fs.readFileSync("file.txt", "utf8")

2. 大文件使用流操作

对于大文件,使用流操作可以避免内存溢出:

javascript
// ✅ 推荐:使用流
const readStream = fs.createReadStream("large-file.txt")
const writeStream = fs.createWriteStream("output.txt")
readStream.pipe(writeStream)

// ❌ 不推荐:一次性读取大文件
const data = fs.readFileSync("large-file.txt") // 可能导致内存溢出

3. 使用 Promise 风格(推荐)

Promise 风格代码更易读,错误处理更清晰:

javascript
// ✅ 推荐:Promise 风格
const fsPromises = require("fs").promises

async function processFile() {
  try {
    const data = await fsPromises.readFile("file.txt", "utf8")
    await fsPromises.writeFile("output.txt", data)
    console.log("处理完成")
  } catch (err) {
    console.error("处理失败:", err)
  }
}

4. 正确处理错误

始终处理可能的错误:

javascript
// ✅ 推荐:完整的错误处理
fs.readFile("file.txt", "utf8", (err, data) => {
  if (err) {
    if (err.code === "ENOENT") {
      console.error("文件不存在")
    } else {
      console.error("读取失败:", err)
    }
    return
  }
  // 处理数据
})

5. 使用 path 模块处理路径

使用 path 模块可以避免跨平台路径问题:

javascript
const fs = require("fs")
const path = require("path")

// ✅ 推荐:使用 path.join
const filePath = path.join(__dirname, "data", "file.txt")
fs.readFile(filePath, "utf8", (err, data) => {
  // ...
})

// ❌ 不推荐:直接拼接路径
const filePath = __dirname + "/data/file.txt" // Windows 上可能出错

6. 性能优化建议

避免不必要的文件操作

javascript
// ❌ 不推荐:重复检查文件存在性
fs.access("file.txt", fs.constants.F_OK, (err) => {
  if (!err) {
    fs.readFile("file.txt", "utf8", (err, data) => {
      // 处理数据
    })
  }
})

// ✅ 推荐:直接操作文件,处理错误
fs.readFile("file.txt", "utf8", (err, data) => {
  if (err) {
    if (err.code === "ENOENT") {
      console.error("文件不存在")
    } else {
      console.error("其他错误:", err)
    }
    return
  }
  // 处理数据
})

使用合适的缓冲区大小

javascript
// 对于大文件流操作,可以调整 highWaterMark
const readStream = fs.createReadStream("large-file.txt", {
  highWaterMark: 1024 * 1024 // 1MB 缓冲区
})

批量操作优化

javascript
// ❌ 不推荐:多次小量写入
for (let i = 0; i < 1000; i++) {
  fs.appendFileSync("log.txt", `Line ${i}\n`)
}

// ✅ 推荐:批量写入
const lines = Array.from({ length: 1000 }, (_, i) => `Line ${i}\n`).join("")
fs.writeFileSync("log.txt", lines)

// ✅ 或使用流
const writeStream = fs.createWriteStream("log.txt")
for (let i = 0; i < 1000; i++) {
  writeStream.write(`Line ${i}\n`)
}
writeStream.end()

7. 跨平台注意事项

路径处理

javascript
const path = require("path")

// ✅ 使用 path 模块处理路径
const filePath = path.join("folder", "subfolder", "file.txt")
// Windows: "folder\\subfolder\\file.txt"
// Unix/Linux/macOS: "folder/subfolder/file.txt"

// ✅ 获取跨平台路径分隔符
const separator = path.sep // Windows: '\\', Unix: '/'

// ✅ 处理不同平台的换行符
const content = "line1\nline2\nline3"
const normalizedContent = content.replace(/\r\n|\r|\n/g, require("os").EOL)

权限处理

javascript
// Windows 上某些权限操作可能无效或表现不同
fs.chmod("file.txt", 0o755, (err) => {
  if (err) {
    // Windows 上可能会失败,需要处理
    console.warn("修改权限失败:", err.message)
  }
})

// 检查平台
if (process.platform === "win32") {
  console.log("Windows 平台,某些文件系统功能可能受限")
}

符号链接创建

javascript
// Windows 创建符号链接可能需要管理员权限
fs.symlink("target.txt", "link.txt", (err) => {
  if (err) {
    if (process.platform === "win32" && err.code === "EPERM") {
      console.error("Windows 创建符号链接需要管理员权限")
      // 可以尝试使用 junction 类型(仅限目录)
    }
  }
})

文件监控差异

javascript
// 不同平台的文件监控行为可能不同
fs.watch("file.txt", (eventType, filename) => {
  // macOS/Windows: filename 可能是相对路径或绝对路径
  // Linux: filename 可能为 null
  if (!filename) {
    console.log("文件发生变化(Linux 平台)")
  } else {
    console.log(`${filename} 发生 ${eventType} 事件`)
  }
})

8. 安全最佳实践

路径遍历攻击防护

javascript
const path = require("path")

function safeReadFile(userPath) {
  // 解析为绝对路径
  const resolvedPath = path.resolve(userPath)
  const allowedDirectory = "/var/www/uploads"

  // 检查路径是否在允许的目录内
  if (!resolvedPath.startsWith(allowedDirectory)) {
    throw new Error("非法路径访问")
  }

  return fsPromises.readFile(resolvedPath, "utf8")
}

敏感文件权限设置

javascript
// 配置文件应该限制访问权限
async function createSecureConfigFile() {
  const fsPromises = require("fs").promises

  await fsPromises.writeFile("config.json", JSON.stringify({ secret: "value" }))

  // 设置为仅所有者可读写
  await fsPromises.chmod("config.json", 0o600)
  console.log("配置文件已创建并设置安全权限")
}

文件上传安全处理

javascript
const fs = require("fs")
const path = require("path")

async function handleFileUpload(filename, tempPath, uploadDir) {
  // 验证文件名(防止路径遍历)
  const safeName = path.basename(filename)
  const targetPath = path.join(uploadDir, safeName)

  // 检查文件类型
  const allowedExtensions = [".jpg", ".png", ".gif", ".pdf"]
  const ext = path.extname(safeName).toLowerCase()

  if (!allowedExtensions.includes(ext)) {
    throw new Error("不支持的文件类型")
  }

  // 移动文件
  await fsPromises.rename(tempPath, targetPath)
  return targetPath
}

临时文件处理

javascript
const fs = require("fs")
const path = require("path")
const os = require("os")

// 使用系统临时目录
const tempFile = path.join(os.tmpdir(), `temp-${Date.now()}.txt`)

// 创建并使用临时文件
fs.writeFile(tempFile, "temporary data", (err) => {
  if (err) throw err

  // 使用完毕后删除
  fs.unlink(tempFile, (err) => {
    if (err) console.error("删除临时文件失败:", err)
  })
})

9. 资源管理

使用 withFileDescriptors(Node.js 18.19.0+)

javascript
const fs = require("fs/promises")

// 自动管理文件描述符
async function readWithAutoClose() {
  try {
    const fileHandle = await fs.open("example.txt", "r")
    // 使用 Symbol.asyncDispose(Node.js 18.19.0+)
    await using file = fileHandle
    const buffer = Buffer.alloc(1024)
    const { bytesRead } = await file.read(buffer, 0, buffer.length, 0)
    return buffer.toString("utf8", 0, bytesRead)
  } catch (err) {
    console.error("读取失败:", err)
  }
}

避免文件描述符泄漏

javascript
const fsPromises = require("fs").promises

// ✅ 推荐:使用 try-finally 确保关闭
async function processFile() {
  let fd
  try {
    fd = await fsPromises.open("file.txt", "r")
    // 执行操作
  } catch (err) {
    console.error("操作失败:", err)
  } finally {
    if (fd) await fd.close()
  }
}

监控打开的文件数量

javascript
// 检查系统允许的最大文件描述符数量
const { promisify } = require("util")
const exec = promisify(require("child_process").exec)

async function checkFileLimits() {
  try {
    if (process.platform !== "win32") {
      const { stdout } = await exec("ulimit -n")
      console.log(`最大文件描述符数量: ${stdout.trim()}`)
    }
  } catch (err) {
    console.log("无法获取系统限制")
  }
}

常见问题解答

Q1: 为什么我的文件读取返回乱码?

问题:使用 readFile 读取文件时,输出的是乱码而不是正常的文本。

原因:没有指定正确的编码格式,默认返回 Buffer 对象。

解决方案

javascript
const fs = require("fs")

// ❌ 错误:没有指定编码,返回 Buffer
fs.readFile("text.txt", (err, data) => {
  console.log(data) // <Buffer ...>
})

// ✅ 正确:指定编码格式
fs.readFile("text.txt", "utf8", (err, data) => {
  console.log(data) // 正常的字符串
})

// 或手动转换 Buffer
fs.readFile("text.txt", (err, data) => {
  console.log(data.toString("utf8"))
})

Q2: 如何处理大文件而不导致内存溢出?

问题:读取大文件时,程序崩溃或内存占用过高。

原因readFile 会将整个文件加载到内存中。

解决方案:使用流(Stream)逐块处理文件。

javascript
const fs = require("fs")
const readline = require("readline")

// 方法1:使用流读取
const readStream = fs.createReadStream("large-file.txt", "utf8")

readStream.on("data", (chunk) => {
  // 处理每个数据块
  console.log(`接收到 ${chunk.length} 字节`)
})

// 方法2:逐行读取
const rl = readline.createInterface({
  input: fs.createReadStream("large-file.txt"),
  crlfDelay: Infinity
})

rl.on("line", (line) => {
  // 处理每一行
})

Q3: 为什么 fs.mkdir 创建多级目录会失败?

问题fs.mkdir("a/b/c") 报错 "ENOENT: no such file or directory"。

原因:默认情况下,mkdir 只能创建单级目录。

解决方案

javascript
const fs = require("fs")

// ✅ 方案1:使用 recursive 选项(Node.js 10.12.0+)
fs.mkdir("a/b/c", { recursive: true }, (err) => {
  if (err) throw err
  console.log("目录创建成功")
})

// ✅ 方案2:手动递归创建
function mkdirp(dirPath) {
  const path = require("path")
  const parts = dirPath.split(path.sep)

  for (let i = 1; i <= parts.length; i++) {
    const current = parts.slice(0, i).join(path.sep)
    if (!fs.existsSync(current)) {
      fs.mkdirSync(current)
    }
  }
}

Q4: 如何正确删除非空目录?

问题fs.rmdir 删除非空目录时报错 "ENOTEMPTY"。

原因rmdir 默认只能删除空目录。

解决方案

javascript
const fsPromises = require("fs").promises
const path = require("path")

// ✅ 方案1:使用 recursive 选项(Node.js 12.10.0+)
fsPromises.rmdir("directory", { recursive: true })

// ✅ 方案2:使用 fs.rm(Node.js 14.14.0+,推荐)
fsPromises.rm("directory", { recursive: true, force: true })

// ✅ 方案3:手动递归删除
async function removeDir(dirPath) {
  const entries = await fsPromises.readdir(dirPath, { withFileTypes: true })

  await Promise.all(
    entries.map((entry) => {
      const fullPath = path.join(dirPath, entry.name)
      return entry.isDirectory() ? removeDir(fullPath) : fsPromises.unlink(fullPath)
    })
  )

  await fsPromises.rmdir(dirPath)
}

Q5: 文件监控为什么在 Linux 上获取不到文件名?

问题fs.watch 在 Linux 平台上,filename 参数为 null

原因:这是 Linux inotify 机制的限制。

解决方案

javascript
const fs = require("fs")

fs.watch("directory", (eventType, filename) => {
  // Linux 平台 filename 可能为 null
  if (!filename) {
    console.log("检测到目录变化,但无法获取文件名")
    // 可以读取整个目录来找出变化的文件
    return
  }
  console.log(`${filename} 发生 ${eventType} 事件`)
})

// 或使用 fs.watchFile(性能较低但跨平台兼容性好)
fs.watchFile("file.txt", (curr, prev) => {
  if (curr.mtime !== prev.mtime) {
    console.log("文件被修改")
  }
})

Q6: 如何判断文件是否存在?

问题:应该使用 exists 还是 access 来检查文件存在性?

原因fs.exists() 已被废弃。

解决方案

javascript
const fs = require("fs")
const fsPromises = require("fs").promises

// ❌ 不推荐:exists 已废弃
fs.exists("file.txt", (exists) => {
  console.log(exists)
})

// ✅ 推荐:使用 access
fs.access("file.txt", fs.constants.F_OK, (err) => {
  console.log(err ? "文件不存在" : "文件存在")
})

// ✅ 推荐:Promise 风格
async function checkExists(path) {
  try {
    await fsPromises.access(path, fs.constants.F_OK)
    return true
  } catch {
    return false
  }
}

// ⚠️ 注意:不要在 exists 检查和实际操作之间有时间窗口
// ✅ 推荐:直接操作,处理错误
fsPromises.readFile("file.txt", "utf8").catch((err) => {
  if (err.code === "ENOENT") {
    console.log("文件不存在")
  }
})

Q7: 如何处理路径中的特殊字符和空格?

问题:文件路径包含空格或特殊字符时,操作失败。

解决方案

javascript
const fs = require("fs")
const path = require("path")

// ✅ 使用 path 模块正确处理路径
const filePath = path.join("folder with spaces", "file name.txt")

// ✅ 正确处理中文路径
const chinesePath = path.join("文件夹", "文件.txt")

// ✅ 处理绝对路径
const absolutePath = path.resolve("relative", "path", "file.txt")

fs.readFile(filePath, "utf8", (err, data) => {
  // 处理数据
})

Q8: 如何正确处理文件编码?

问题:不同编码的文件(GBK、UTF-8 with BOM)读取时出现乱码。

解决方案

javascript
const fs = require("fs")
const iconv = require("iconv-lite") // 需要安装:npm install iconv-lite

// 读取非 UTF-8 编码的文件
fs.readFile("gbk-file.txt", (err, data) => {
  if (err) throw err
  // 使用 iconv-lite 解码
  const text = iconv.decode(data, "gbk")
  console.log(text)
})

// 处理 UTF-8 with BOM
fs.readFile("utf8-bom.txt", "utf8", (err, data) => {
  if (err) throw err
  // 移除 BOM(EFBBBF)
  const text = data.replace(/^\uFEFF/, "")
  console.log(text)
})

// 写入不同编码
const content = "中文内容"
const buffer = iconv.encode(content, "gbk")
fs.writeFile("gbk-output.txt", buffer, (err) => {
  if (err) throw err
  console.log("GBK 文件写入成功")
})

Q9: 为什么 Windows 上创建符号链接失败?

问题:在 Windows 上调用 fs.symlink 时报错 "EPERM: operation not permitted"。

原因:Windows 创建符号链接需要管理员权限或开发者模式。

解决方案

javascript
const fs = require("fs")

// ✅ 方案1:使用 junction 类型(仅限目录,不需要管理员权限)
fs.symlink("target-dir", "link-dir", "junction", (err) => {
  if (err) throw err
  console.log("目录连接创建成功")
})

// ✅ 方案2:检查权限并提示用户
fs.symlink("target.txt", "link.txt", (err) => {
  if (err && err.code === "EPERM" && process.platform === "win32") {
    console.error("创建符号链接需要管理员权限")
    console.error("请以管理员身份运行,或启用 Windows 开发者模式")
    return
  }
  console.log("符号链接创建成功")
})

// ✅ 方案3:使用硬链接替代(文件)
fs.link("target.txt", "hardlink.txt", (err) => {
  if (err) throw err
  console.log("硬链接创建成功")
})

Q10: 如何避免回调地狱(Callback Hell)?

问题:多个异步文件操作嵌套,代码难以维护。

解决方案

javascript
const fsPromises = require("fs").promises
const path = require("path")

// ❌ 回调地狱
fs.readFile("file1.txt", "utf8", (err, data1) => {
  if (err) throw err
  fs.writeFile("file2.txt", data1, (err) => {
    if (err) throw err
    fs.readFile("file3.txt", "utf8", (err, data3) => {
      if (err) throw err
      // 更多嵌套...
    })
  })
})

// ✅ 使用 Promise 链
fsPromises.readFile("file1.txt", "utf8")
  .then(data1 => fsPromises.writeFile("file2.txt", data1))
  .then(() => fsPromises.readFile("file3.txt", "utf8"))
  .then(data3 => console.log(data3))
  .catch(err => console.error("操作失败:", err))

// ✅ 使用 async/await(推荐)
async function processFiles() {
  try {
    const data1 = await fsPromises.readFile("file1.txt", "utf8")
    await fsPromises.writeFile("file2.txt", data1)
    const data3 = await fsPromises.readFile("file3.txt", "utf8")
    console.log(data3)
  } catch (err) {
    console.error("操作失败:", err)
  }
}

// ✅ 并行执行多个操作
async function processMultipleFiles() {
  try {
    const [file1, file2, file3] = await Promise.all([
      fsPromises.readFile("file1.txt", "utf8"),
      fsPromises.readFile("file2.txt", "utf8"),
      fsPromises.readFile("file3.txt", "utf8")
    ])
    console.log(file1, file2, file3)
  } catch (err) {
    console.error("读取失败:", err)
  }
}

Q11: 如何监听文件的创建事件?

问题fs.watch 无法监听到新文件的创建。

解决方案

javascript
const fs = require("fs")
const path = require("path")

// 监听目录,可以检测到文件创建
fs.watch("directory", (eventType, filename) => {
  const fullPath = path.join("directory", filename)

  if (eventType === "rename") {
    // 文件被创建或删除
    fs.access(fullPath, fs.constants.F_OK, (err) => {
      if (err) {
        console.log(`文件 ${filename} 被删除`)
      } else {
        console.log(`文件 ${filename} 被创建`)
      }
    })
  } else if (eventType === "change") {
    console.log(`文件 ${filename} 被修改`)
  }
})

// 递归监听(仅支持 macOS 和 Windows)
fs.watch("directory", { recursive: true }, (eventType, filename) => {
  console.log(`${filename} 发生 ${eventType} 事件`)
})

Q12: 如何获取文件的完整信息(包括权限、所有者等)?

解决方案

javascript
const fs = require("fs")
const fsPromises = require("fs").promises

// 使用 stat 获取详细信息
fs.stat("file.txt", (err, stats) => {
  if (err) throw err

  console.log("=== 文件信息 ===")
  console.log(`文件大小: ${stats.size} 字节`)
  console.log(`是否为文件: ${stats.isFile()}`)
  console.log(`是否为目录: ${stats.isDirectory()}`)
  console.log(`创建时间: ${stats.birthtime}`)
  console.log(`修改时间: ${stats.mtime}`)
  console.log(`访问时间: ${stats.atime}`)
  console.log(`状态改变时间: ${stats.ctime}`)

  // 权限信息
  const mode = stats.mode
  console.log(`权限模式: ${(mode & 0o777).toString(8).padStart(3, "0")}`)

  // 所有者信息(Unix/Linux)
  console.log(`用户 ID: ${stats.uid}`)
  console.log(`组 ID: ${stats.gid}`)

  // 硬链接数
  console.log(`硬链接数: ${stats.nlink}`)
})

// Promise 风格
async function getFileInfo(path) {
  try {
    const stats = await fsPromises.stat(path)
    return {
      size: stats.size,
      isFile: stats.isFile(),
      isDirectory: stats.isDirectory(),
      created: stats.birthtime,
      modified: stats.mtime,
      permissions: (stats.mode & 0o777).toString(8)
    }
  } catch (err) {
    console.error("获取文件信息失败:", err)
    throw err
  }
}

Q13: 如何处理文件锁定?

问题:多个进程同时访问文件,导致数据冲突。

解决方案

javascript
// Node.js 原生不支持文件锁定,可以使用第三方库
// 推荐使用 proper-lockfile 或 fs-extra

const lockfile = require("proper-lockfile") // npm install proper-lockfile

async function safeWriteFile(filepath, data) {
  // 获取文件锁
  const release = await lockfile.lock(filepath)

  try {
    // 执行文件操作
    await fsPromises.writeFile(filepath, data)
    console.log("文件写入成功")
  } finally {
    // 释放锁
    await release()
  }
}

// 使用排他标志(简单场景)
fs.open("file.txt", "wx", (err, fd) => {
  if (err) {
    if (err.code === "EEXIST") {
      console.log("文件已被其他进程锁定")
    }
    return
  }
  // 文件操作
  fs.close(fd, () => {})
})

Q14: 如何正确处理文件路径中的符号链接?

解决方案

javascript
const fs = require("fs")
const path = require("path")

// realpath:解析符号链接,返回真实路径
fs.realpath("symlink-dir", (err, resolvedPath) => {
  if (err) throw err
  console.log("符号链接指向的真实路径:", resolvedPath)
})

// lstat:获取符号链接本身的信息(而不是目标文件)
fs.lstat("symlink", (err, stats) => {
  if (err) throw err
  if (stats.isSymbolicLink()) {
    console.log("这是一个符号链接")
  }
})

// 读取符号链接的目标
fs.readlink("symlink", (err, linkString) => {
  if (err) throw err
  console.log("符号链接指向:", linkString)
})

// 判断路径是否存在循环引用
function checkCircularLink(filepath) {
  const visited = new Set()
  let current = filepath

  while (true) {
    if (visited.has(current)) {
      throw new Error("检测到循环引用")
    }
    visited.add(current)

    try {
      const stats = fs.lstatSync(current)
      if (!stats.isSymbolicLink()) {
        break
      }
      const target = fs.readlinkSync(current)
      current = path.resolve(path.dirname(current), target)
    } catch (err) {
      break
    }
  }

  return current
}

Q15: 如何处理文件系统事件丢失的问题?

问题:在高频文件操作场景下,fs.watch 可能会丢失事件。

解决方案

javascript
const fs = require("fs")
const { debounce } = require("lodash") // npm install lodash

// 使用防抖处理高频事件
const watcher = fs.watch("file.txt", (eventType, filename) => {
  // 快速连续的事件可能只触发一次
  console.log(`${eventType} 事件触发`)
})

// 使用防抖函数减少事件触发频率
let lastContent = ""

const debouncedHandler = debounce(() => {
  fs.readFile("file.txt", "utf8", (err, content) => {
    if (err) throw err
    if (content !== lastContent) {
      lastContent = content
      console.log("文件内容已变化")
    }
  })
}, 100)

fs.watch("file.txt", debouncedHandler)

// 使用轮询作为备选方案
setInterval(() => {
  fs.stat("file.txt", (err, stats) => {
    if (err) return
    // 检查文件变化
  })
}, 1000)

补充说明

Node.js 版本兼容性

不同 Node.js 版本对 fs 模块的支持:

功能最低版本说明
Promise API10.0.0fs.promises API
recursive mkdir10.12.0{ recursive: true } 选项
recursive rmdir12.10.0{ recursive: true } 选项
fs.rm14.14.0新的删除 API,推荐使用
FileHandle10.0.0fsPromises.open() 返回的对象
withFileDescriptors18.19.0using 关键字支持

相关模块

  • path:处理文件路径,跨平台兼容
  • stream:流式处理大文件
  • readline:逐行读取文件
  • os:获取系统信息(临时目录、换行符等)
  • crypto:文件加密和哈希
  • zlib:文件压缩和解压

性能基准

典型操作的性能对比(仅供参考):

操作同步 API回调 APIPromise API流操作
小文件读取较快不推荐
大文件读取内存占用高内存占用高内存占用高推荐
批量文件操作阻塞严重性能良好性能良好推荐

调试技巧

javascript
// 启用 fs 模块的调试日志(需要设置环境变量)
// NODE_DEBUG=fs node script.js

// 使用 util.inspect 查看详细错误信息
const util = require("util")

fs.readFile("file.txt", (err, data) => {
  if (err) {
    console.error(util.inspect(err, { depth: null }))
  }
})

// 使用 Error.captureStackTrace 获取调用栈
function readFileWithTrace(path) {
  const stack = {}
  Error.captureStackTrace(stack, readFileWithTrace)

  fs.readFile(path, (err, data) => {
    if (err) {
      err.stack = stack.stack + "\n" + err.stack
      throw err
    }
  })
}

扩展阅读


Node.js 22+ fs 模块新特性

glob 文件模式匹配

Node.js 22+ 内置 fs.globfs.globSync,无需安装 glob 包:

javascript
import { glob } from 'node:fs/promises'
import { globSync } from 'node:fs'

// 异步遍历匹配文件
for await (const file of glob('./src/**/*.js')) {
  console.log(file)
}

// 同步获取匹配文件列表
const files = globSync('./src/**/*.ts')
console.log(files)

// 带选项的 glob
for await (const file of glob('./**/*.md', {
  cwd: './docs',
  exclude: (path) => path.includes('node_modules')
})) {
  console.log(file)
}

权限模型与 fs

Node.js 22.13+ 权限模型稳定后,fs 操作受权限控制:

bash
# 限制文件系统读取范围
node --permission --allow-fs-read=./data --allow-fs-read=./config app.js

# 限制文件系统写入范围
node --permission --allow-fs-write=./output app.js

# 允许所有读取,限制写入
node --permission --allow-fs-read=* --allow-fs-write=./tmp app.js
javascript
// 在代码中检查权限
if (process.permission.has('fs.read', './data/secret.txt')) {
  const data = await fs.promises.readFile('./data/secret.txt', 'utf-8')
} else {
  console.error('无权限读取该文件')
}

fs.promises 完整支持

Node.js 22+ 的 fs.promises 已完全稳定,推荐使用:

javascript
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises'

// 使用 using 关键字自动关闭文件句柄(Node.js 22+)
{
  using file = await open('./data.txt', 'r')
  const content = await file.readFile('utf-8')
} // 文件句柄自动关闭