fs 文件系统模块
fs 模块是 Node.js 中内置的文件系统模块,使用它可以对本地的文件及目录(文件夹)进行操作。fs 是 file system 的简写,表示文件系统。
const fs = require("fs")模块特性
核心功能
- 文件操作:读取、写入、删除、复制、重命名、截断文件
- 目录操作:创建、读取、删除目录,获取目录信息
- 流操作:支持流式读写,适合处理大文件
- 文件监控:实时监控文件和目录的变化
- 权限管理:修改文件权限和所有权
- 符号链接:创建和读取符号链接
- 文件描述符:底层文件操作接口
跨平台兼容性
fs 模块提供了跨平台的文件系统操作接口,但需要注意:
- 路径分隔符:Windows 使用
\,Unix/Linux/macOS 使用/(建议使用path模块) - 权限模式:Windows 和 Unix 权限模型不同
- 文件监控:不同平台的文件系统事件机制有差异
- 符号链接:Windows 需要管理员权限创建符号链接
性能考虑
| API 风格 | 性能特点 | 适用场景 |
|---|---|---|
| 同步 API | 阻塞事件循环,性能较低 | 启动脚本、CLI 工具、简单脚本 |
| 回调 API | 异步非阻塞,性能良好 | 兼容旧代码、事件驱动场景 |
| Promise API | 异步非阻塞,代码清晰 | 现代应用、推荐使用 |
三种 API 风格
fs 模块提供了三种不同的 API 风格:
- 回调函数风格(Callback):传统的异步 API,使用回调函数处理结果
- 同步风格(Sync):同步 API,会阻塞事件循环
- Promise 风格(Promises):基于 Promise 的异步 API(推荐)
// 回调函数风格
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 捕获错误,避免程序崩溃:
let data
try {
data = fs.readFileSync('./config.json')
} catch (err) {
console.log('配置文件读取出错,及时检查服务')
// 其他错误处理,如 log(err)、通知管理员等
}从回调地狱到 async/await 的演进
当使用异步回调方式编写代码时,如果多个异步操作存在依赖关系,就会面临"回调地狱"(Callback Hell)的问题:
// 回调地狱示例
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 改造:
// 将 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 改造(推荐):
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对象值见下表
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%;" />fs 模块提供对文件与目录进行操作的方法时,通常分别提供同步方法和异步方法,其中,同步方法名通常是在异步方法名后面加了 Sync 后缀,如 access() 方法的对应同步方法为 accessSync(),但除了文件读写操作,一般都默认使用异步方法
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 文件是否存在且是否可写的代码如下:
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 风格:
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()access() 方法不仅可以检测文件是否存在,也可以检测文件夹是否存在
文件读取 readFile
fs 模块为读取文件提供了两个方法,即 readFile() 方法和 readFileSync() 方法,二者的区别是,前者为异步读取文件(默认操作),后者为同步读取文件,这两个方法的语法格式如下:
file:文件名encoding:文件的编码格式callback:回调函数
fs.readFile(file, encoding, callback)
fs.readFileSync(file, encoding)读取文件
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 方法的完整语法:
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:
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 循环中所有的内容都放在了匿名函数中,并且该匿名函数需要自动执行,这样在执行该文件时,保证了每次循环都会输出一句歌词
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 文件内容
[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
这两个方法分别用来对文件进行异步和同步写入,它们的语法格式如下:
fs.writeFile(file, data[, options], callback)
fs.writeFileSync(file, data[, options])参数说明:
file:文件名或文件描述符data:写入文件的内容,可以是字符串、Buffer 或 Uint8Arrayoptions:可选参数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+,但文件存在时失败) | 创建新文件 | 文件结尾处 |
r 和 r+ 模式下,如果文件不存在会抛出 ENOENT 错误;而 w、w+、a、a+ 模式下,文件不存在时会自动创建。x 系列标志用于确保不会意外覆盖已有文件。
示例:创建文件并且向文件中写入内容
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 数据:
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
这两个方法分别向文件异步追加内容和同步追加内容,它们的语法格式如下:
fs.appendFile(path, data[, options], callback)
fs.appendFileSync(path, data[, options])参数说明:
path:文件路径data:要写入文件的数据options:可选参数encoding:编码方式,默认值为"utf8"mode:文件模式,默认值为0o666flag:文件系统标志,默认值为"a"(追加模式)
callback:回调函数,参数为(err)
示例:
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 进行异常处理:
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. 异步操作的异常处理
使用回调函数风格的异步方法时,错误作为第一个参数传递:
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):
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 | 磁盘空间不足 |
错误处理示例:
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:用于指定截断文件操作完毕时执行的回调函数,该回调函数中使用一个参数,参数值为截断文件操作失败时触发的错误对象
fs.truncate(path[, len], callback)注意当 len 为 0 时,说明文件的内容为空
示例:
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 + "字节。")
})
}
})删除文件 unlink
在 fs 模块中,可以使用 unlink() 方法对文件进行删除操作,其语法格式如下:
- path:用于指定被删除文件的路径
- callback:回调函数
fs.unlink(path, callback)示例:删除指定被路径下的文本文件
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:复制操作的修饰符,默认值为 0callback:回调函数
fs.copyFile(src, dest[, mode], callback)
fs.copyFileSync(src, dest[, mode])示例:
var fs = require("fs")
fs.copyFile("poems.txt", "poems1.txt", function (err) {
if (err) {
console.log("复制文件失败")
console.log(err)
} else {
console.log("复制文件成功")
}
})readFile、writeFile
除了上面介绍的直接复制文件的方式,还可以通过复制文件内容的方式实现文件的复制,这需要使用 fs 模块中的 readFile() 方法和 writeFile() 方法
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:回调函数
fs.rename(oldPath, newPath, callback)使用示例:
fs = require("fs")
fs.rename("poems.txt", "春夜喜雨.txt", function (err) {
if (err) {
console.log("糟糕!重命名文件失败")
console.log(err)
} else {
console.log("重命名文件成功")
}
})示例:批量重命名文件
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:指定创建目录操作完毕时调用的回调函数,该回调函数中只有一个参数,参数值为创建目录操作失败时触发的错误对象
fs.mkdir(path[, options], callback)
fs.mkdirSync(path[, options])示例:批量创建文件并放到指定的文件夹中
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("创建文件失败")
})
}
})注意的是,创建目录时需要一级一级地创建,而不是直接创建多级目录
读取目录 readdir
在 fs 模块中,可以使用 readdir() 方法或者 readdirSync() 方法读取目录
path:文件名或者文件描述符options:可选参数,可以为如下值encoding:编码方式flag:文件系统标志signal:允许中止正在进行的读取文件操作
callback:回调函数,在回调函数中有两个参数err:出现错误时的错误信息data:调用成功时的返回值
fs.readdir(path[, options], callback)示例:
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,则执行递归目录删除操作。在递归模式下,操作将在失败时重试。默认值为 falseretryDelay:重试之间等待的时间(以毫秒为单位)。如果 recursive 选项不为 true,则忽略此选项。默认值为 100maxRetries:表示重试次数,如果遇到EBUSY、EMFILE、ENFILE、ENOTEMPTY或EPERM错误,Node.js 将在每次尝试时,以retryDelay毫秒的线性退避等待时间重试该操作。如果recursive选项不为true,则忽略此选项。默认值为 0
callback:用于指定删除目录操作完毕时调用的回调函数
fs.rmdir(path[, options], callback)如果目录不为空,fs.rmdir 默认会抛出错误。可以设置选项 { recursive: true } 来允许递归删除非空目录
以下是 fs.rmdir 的使用示例:
1. 基本用法:
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 方法:
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 的同步版本,或者在异步版本中检查目录是否为空:
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() 方法的语法格式如下:、
stat(path, callback)
lstat(path, callback)示例:
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 协议 |
示例:
const fs = require("fs")
fs.stat("b.txt", function (err, stats) {
console.log("是否为文件", stats.isFile())
console.log("是否为文件夹/目录", stats.isDirectory())
})获取目录的绝对路径 realpath
在 fs 模块中,可以使用 realpath() 方法或 realpathSync() 方法获取指定目录的绝对路径,其语法格式如下:
path:路径,可以为字符串或者urloptions:一般为encoding,用于指定编码格式,默认为utf8callback:回调函数,该回调函数中有两个参数err:发生错误时的错误信息resolvedPath:绝对路径
fs.realpath(path[, options], callback)示例:
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
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:文件权限(创建文件时),默认为0o666callback:回调函数,参数为(err, fd)
使用示例:
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
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)
使用示例:
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
})
})
})精细化读取示例:从文件指定位置读取指定长度的数据
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 格式:
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
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 数据):
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
})
})
})精细化写入示例:向文件指定位置写入数据
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() 的第二种用法是直接写入字符串:
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) {})
})
})多次对同一文件进行 write 操作并不安全,必须等待回调函数执行完成才能进行下一次写入。官方推荐使用 createWriteStream 流方式替代,详见 流操作(Stream) 章节。
关闭文件描述符 close
fs.close(fd, callback)
fs.closeSync(fd)
fsPromises.close(fd)重要提示:
使用完文件描述符后,务必关闭文件,否则会导致资源泄漏。推荐使用 try-finally 或 try-with-resources 模式确保文件被关闭。
最佳实践:
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/appendFile | open + read/write + close |
| 操作次数 | 一次性完成 | 可多次读写 |
| 文件关闭 | 内部自动调用 close | 需手动调用 close |
| 位置控制 | 不支持指定读写位置 | 支持通过 position 参数精确控制 |
| 适用场景 | 简单的文件读写、小文件 | 需要多次读写、指定位置读写、大文件分块处理 |
选择建议:
- 如果只需一次性读取或写入整个文件,使用
readFile/writeFile/appendFile即可,它们内部会自动管理文件关闭 - 如果需要多次对同一文件进行读写操作,或需要精确控制读写位置,应使用
open+read/write+close的精细化方式 - 对于大文件或高频写入场景,推荐使用 Stream 方式(
createReadStream/createWriteStream),详见 流操作(Stream) 章节
文件权限操作
修改文件权限 chmod
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):所有用户都有完整权限(不推荐)
使用示例:
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
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)
使用示例:
const fs = require("fs")
// 将文件所有者改为用户 ID 为 1000,组 ID 为 1000
fs.chown("file.txt", 1000, 1000, (err) => {
if (err) {
console.error("修改所有者失败:", err)
return
}
console.log("所有者修改成功")
})修改文件所有者通常需要 root 权限或文件所有者权限。在 Windows 系统上,此方法可能无效或表现不同。
获取文件权限信息
通过 stat() 方法可以获取文件的权限信息:
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}`)
})链接操作
创建硬链接 link
硬链接是文件的另一个名称,指向相同的 inode。删除原文件不会影响硬链接。
fs.link(existingPath, newPath, callback)
fs.linkSync(existingPath, newPath)
fsPromises.link(existingPath, newPath)使用示例:
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
- 删除原文件不会删除硬链接
- 硬链接不能跨文件系统
- 硬链接不能指向目录(大多数系统)
创建符号链接 symlink
符号链接(软链接)是特殊的文件,包含指向另一个文件的路径。
fs.symlink(target, path[, type], callback)
fs.symlinkSync(target, path[, type])
fsPromises.symlink(target, path[, type])参数说明:
target:链接指向的目标路径path:符号链接的路径type:链接类型(仅 Windows)"file":文件链接"dir":目录链接"junction":Windows 目录连接点
使用示例:
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 创建符号链接可能需要管理员权限
读取符号链接目标 readlink
fs.readlink(path[, options], callback)
fs.readlinkSync(path[, options])
fsPromises.readlink(path[, options])使用示例:
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() 方法可以获取符号链接本身的信息(而不是目标文件的信息):
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
fs.createReadStream(path[, options])常用选项:
encoding:编码格式,默认值为null(返回 Buffer)flags:文件系统标志,默认值为"r"start:开始读取的位置(字节)end:结束读取的位置(字节)highWaterMark:缓冲区大小,默认值为 64KB
基本使用:
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
fs.createWriteStream(path[, options])常用选项:
encoding:编码格式,默认值为"utf8"flags:文件系统标志,默认值为"w"start:开始写入的位置(字节)highWaterMark:缓冲区大小,默认值为 16KB
基本使用:
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() 方法可以高效地复制大文件:
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:逐行读取大文件:
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:流式写入数据:
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() 方法可以监控文件的变化,当文件被修改时会触发回调:
fs.watchFile(filename[, options], listener)参数说明:
filename:要监控的文件路径options:可选参数interval:轮询间隔(毫秒),默认值为 5007persistent:是否持续监控,默认值为true
listener:回调函数,参数为(currentStats, previousStats)
使用示例:
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() 更高效:
fs.watch(filename[, options][, listener])参数说明:
filename:要监控的文件或目录路径options:可选参数recursive:是否递归监控子目录(仅限 macOS 和 Windows),默认值为falseencoding:用于指定传递给监听器的文件名编码,默认值为"utf8"
listener:回调函数,参数为(eventType, filename)
事件类型:
"rename":文件或目录被重命名或删除"change":文件被修改
使用示例:
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()实际应用示例:自动重启服务器:
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,并监听文件变化自动更新。
功能需求:
- 读取 md 和 css 内容
- 将读取的内容替换占位符,生成最终的 HTML 字符串
- 将 HTML 字符串写入到指定的 HTML 文件中
- 监听 md 文档内容的变化,然后更新 HTML 内容
- 使用 browser-sync 来实时显示 HTML 内容
实现代码:
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 中数据写入到目标文件中 - 循环读取和写入,直到文件拷贝完成
实现代码:
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()
})
})优化版本(使用流操作):
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:递归创建目录
实现一个函数,可以递归创建多级目录
- 同步实现
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")- 异步实现(回调函数风格)
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("目录创建完成")
})- Promise 风格实现
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:递归删除目录
实现一个函数,可以递归删除目录及其所有内容。
实现思路:
- 判断当前传入的路径是否为文件,是则直接删除
- 如果当前传入的是一个目录,需要继续读取目录中的内容,然后再执行删除操作
- 将删除行为定义成一个函数,通过递归的方式进行复用
- 将当前的文件名拼接成在删除时可使用的路径
实现代码:
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 风格实现:
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:
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:
{
"signup": "注册"
}pageb.json:
{
"menu": "菜单"
}希望将它们合并成 data.json:
{
"pagea": {
"signup": "注册"
},
"pageb": {
"menu": "菜单"
}
}实现代码:
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()使用方法:
# 假设翻译文件存放在 ./i18n/zh 目录下
node merge.js ./i18n/zh
# 合并结果将输出到当前目录的 data.json功能扩展建议:
- 支持递归遍历子目录:修改
walk函数,支持递归读取子目录中的 JSON 文件 - 支持多语言合并:遍历多个语言目录,生成按语言区分的合并文件
- 添加文件监控:使用
fs.watch监控文件变化,自动重新合并 - 支持增量更新:记录文件修改时间,只合并有变化的文件
递归遍历版本:
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. 优先使用异步方法
异步方法不会阻塞事件循环,适合生产环境:
// ✅ 推荐:异步方法
fs.readFile("file.txt", "utf8", (err, data) => {
// 处理数据
})
// ❌ 不推荐:同步方法(会阻塞)
const data = fs.readFileSync("file.txt", "utf8")2. 大文件使用流操作
对于大文件,使用流操作可以避免内存溢出:
// ✅ 推荐:使用流
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 风格代码更易读,错误处理更清晰:
// ✅ 推荐: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. 正确处理错误
始终处理可能的错误:
// ✅ 推荐:完整的错误处理
fs.readFile("file.txt", "utf8", (err, data) => {
if (err) {
if (err.code === "ENOENT") {
console.error("文件不存在")
} else {
console.error("读取失败:", err)
}
return
}
// 处理数据
})5. 使用 path 模块处理路径
使用 path 模块可以避免跨平台路径问题:
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. 性能优化建议
避免不必要的文件操作
// ❌ 不推荐:重复检查文件存在性
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
}
// 处理数据
})使用合适的缓冲区大小
// 对于大文件流操作,可以调整 highWaterMark
const readStream = fs.createReadStream("large-file.txt", {
highWaterMark: 1024 * 1024 // 1MB 缓冲区
})批量操作优化
// ❌ 不推荐:多次小量写入
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. 跨平台注意事项
路径处理
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)权限处理
// Windows 上某些权限操作可能无效或表现不同
fs.chmod("file.txt", 0o755, (err) => {
if (err) {
// Windows 上可能会失败,需要处理
console.warn("修改权限失败:", err.message)
}
})
// 检查平台
if (process.platform === "win32") {
console.log("Windows 平台,某些文件系统功能可能受限")
}符号链接创建
// Windows 创建符号链接可能需要管理员权限
fs.symlink("target.txt", "link.txt", (err) => {
if (err) {
if (process.platform === "win32" && err.code === "EPERM") {
console.error("Windows 创建符号链接需要管理员权限")
// 可以尝试使用 junction 类型(仅限目录)
}
}
})文件监控差异
// 不同平台的文件监控行为可能不同
fs.watch("file.txt", (eventType, filename) => {
// macOS/Windows: filename 可能是相对路径或绝对路径
// Linux: filename 可能为 null
if (!filename) {
console.log("文件发生变化(Linux 平台)")
} else {
console.log(`${filename} 发生 ${eventType} 事件`)
}
})8. 安全最佳实践
路径遍历攻击防护
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")
}敏感文件权限设置
// 配置文件应该限制访问权限
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("配置文件已创建并设置安全权限")
}文件上传安全处理
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
}临时文件处理
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+)
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)
}
}避免文件描述符泄漏
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()
}
}监控打开的文件数量
// 检查系统允许的最大文件描述符数量
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 对象。
解决方案:
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)逐块处理文件。
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 只能创建单级目录。
解决方案:
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 默认只能删除空目录。
解决方案:
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 机制的限制。
解决方案:
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() 已被废弃。
解决方案:
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: 如何处理路径中的特殊字符和空格?
问题:文件路径包含空格或特殊字符时,操作失败。
解决方案:
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)读取时出现乱码。
解决方案:
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 创建符号链接需要管理员权限或开发者模式。
解决方案:
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)?
问题:多个异步文件操作嵌套,代码难以维护。
解决方案:
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 无法监听到新文件的创建。
解决方案:
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: 如何获取文件的完整信息(包括权限、所有者等)?
解决方案:
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: 如何处理文件锁定?
问题:多个进程同时访问文件,导致数据冲突。
解决方案:
// 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: 如何正确处理文件路径中的符号链接?
解决方案:
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 可能会丢失事件。
解决方案:
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 API | 10.0.0 | fs.promises API |
| recursive mkdir | 10.12.0 | { recursive: true } 选项 |
| recursive rmdir | 12.10.0 | { recursive: true } 选项 |
| fs.rm | 14.14.0 | 新的删除 API,推荐使用 |
| FileHandle | 10.0.0 | fsPromises.open() 返回的对象 |
| withFileDescriptors | 18.19.0 | using 关键字支持 |
相关模块
path:处理文件路径,跨平台兼容stream:流式处理大文件readline:逐行读取文件os:获取系统信息(临时目录、换行符等)crypto:文件加密和哈希zlib:文件压缩和解压
性能基准
典型操作的性能对比(仅供参考):
| 操作 | 同步 API | 回调 API | Promise API | 流操作 |
|---|---|---|---|---|
| 小文件读取 | 较快 | 快 | 快 | 不推荐 |
| 大文件读取 | 内存占用高 | 内存占用高 | 内存占用高 | 推荐 |
| 批量文件操作 | 阻塞严重 | 性能良好 | 性能良好 | 推荐 |
调试技巧
// 启用 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 fs 模块官方文档
- Node.js File System Best Practices
- Understanding File Descriptors
- Linux File Permissions Guide
Node.js 22+ fs 模块新特性
glob 文件模式匹配
Node.js 22+ 内置 fs.glob 和 fs.globSync,无需安装 glob 包:
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 操作受权限控制:
# 限制文件系统读取范围
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// 在代码中检查权限
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 已完全稳定,推荐使用:
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')
} // 文件句柄自动关闭