原文来自 我的个人博客
前言
对于任何一个为服务端服务的语言或者框架来说,通常都会有自己的文件系统:
- 因为服务器需要将各种数据、文件等放置到不同的地方;
- 比如用户数据可能大多数都是放到数据库的;
- 比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在操作系统上的
而 Node 也有自己的文件系统操作模块。就是 fs(File System) 。
借助于 Node 帮我们封装的文件系统,我们可以在任何的操作系统(window、Mac OS、Linus)上面直接去操作文件。这也是 Node 可以开发服务器的一大原因,也是它可以成为前端自动化脚本等热门工具的原因。
Node 文件系统的 API 非常多,这些 API大多数都提供了三种操作方式:
- 同步操作文件:代码会被阻塞,不会继续执行;
- 异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当返回结果时,回调函数被执行;
- 异步
Promise操作文件:代码不会被阻塞,通过fs.promises调用方法操作,会返回一个Promise,可以通过then、catcth进行处理
本章我们呢会介绍最常用的几种 API。贴一下 文档地址,没介绍到的就去查文档吧。
1. 文件的读取
我们先来一个最简单的需求,如果我们想要读取一个文件,该怎么操作呢?
Node 给我们提供了三种读取文件的方式:
我们先创建一个 aaa.txt 的文件,随便写入一行 Hello World,接着调用 api 读取这个文件的内容。
1.1. readFileSync (同步读取)
第一种读取文件的方式是通过 readFileSync 同步读取文件:
// 引入 fs 模块
const fs = require("fs");
// 1. 同步读取
const file = fs.readFileSync("./aaa.txt");
console.log(file);
console.log("后续的代码");

可以看到打印的第一行的 Buffer 就是我们读取的文件了,后续代码在读取到文件后执行,说明了 readFileSync 确实是同步执行会阻塞后续代码的
关于 Buffer 我们后续再讲,如果想看到具体的文本,我们有两种方式实现。
- 在
file后加上toString()
// 1.1. 同步读取 toString
const file1 = fs.readFileSync("./aaa.txt");
console.log(file1.toString());
console.log("后续的代码");
readFileSync第二个参数中添加encoding: 'utf-8'
// 1.2. 同步读取 toString
const file2 = fs.readFileSync("./aaa.txt", {
encoding: "utf-8",
});
console.log(file2);
console.log("后续的代码");
encoding 参数的作用是在读取文件时定义以什么样的编码读取文件,如果不传的话,它就会将文件当成一个二进制的文件(最终会转换为十六进制表示)
这两种方法最终都能打印出 Hello World

1.2 readFile(异步 + 回调函数)
第二种读取文件的方式是通过 异步 + 回调 的方式
// 2.1 异步 + 回调函数 readFile
fs.readFile("./aaa.txt", { encoding: "utf-8" }, (err, data) => {
if (err) {
console.error("读取文件错误", err);
return
}
console.log(data);
});
console.log("后续的代码");
readFile 的前两个参数与 readFileSync 相同,它的第三个参数是一个回调函数,当读取到文件的时候会执行这个函数,这个函数有两个参数 err 和 data

可以看到先打印 “后续的代码”,说明 readFile 读取文件是没有阻塞后续代码的
1.3 readFile (异步 + Promise)
我们知道异步读取的结果如果通过回调的方式处理的话,非常容易产生回调地狱的问题。
自从有了 Pormise 后,Node 也支持了通过 Promise 的方式处理异步问题。
// 3.1 异步 + Promise
fs.promises
.readFile("./aaa.txt", { encoding: "utf-8" })
.then((res) => {
console.log(res.toString());
})
.catch((err) => {
console.error("读取文件错误", err);
});
console.log("后续的代码");
fs.promises.readFile 与 fs.readFile 的前两个参数都相同,只不过前者返回的是一个 Promise。
1.4 总结
这一小节我们介绍了 fs 读取文件的三种方法,一种 同步,一种 异步+回调,一种 异步+Primise,Node 中很多 API 都会有这三种方法,如果出现 fs.xxx、fs.xxxSync、fs.promises.xxx 都是同理的大家都应该能明白了。
2. 文件描述符
什么是文件描述符(File descriptors)?
- 在常见的操作系统上,对于每个进程,内核都维护着一张当前打开着的文件的表格
- 在每个打开的文件都分配了一个称为文件描述符的简单的数字标识符
- 在系统层,所有文件系统操作都是用这些文件描述符来标识和跟踪每个特定的文件
Windows系统使用了一个虽然不同但概念上类似的机制来跟踪资源。
为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配了一个数字型的文件描述符。
2.1 open 获取文件描述符
我们可以通过 fs.open() 方法是打开某个文件,为其分配文件描述符
const fs = require("fs");
fs.open("./aaa.txt", (err, fd) => {
console.log(fd);
});

可以看到此时 aaa.txt 的文件描述符为 20
文件描述符,可用于从文件读取数据、写入数据、或请求相关文件的信息
2.2 文件描述符的使用
- 读取数据
上面讲的读取文件 API 的第一个参数不仅可以是一个路径,也可以是一个文件描述符,如下图所示:

const fs = require("fs");
// 可以把 1 当成是对 2 的封装
// 1
const file = fs.readFileSync('./aaa.txt')
// 2
fs.open("./aaa.txt", (err, fd) => {
const file1 = fs.readFileSync(fd)
});
- 通过
fstat请求文件相关信息
const fs = require("fs");
fs.open("./aaa.txt", (err, fd) => {
fs.fstat(fd, (err, stats) => {
if (err) return;
console.log(stats);
// 通过 open 打开的文件不会默认关闭掉,通常需要我们手动关闭
fs.close(fd);
});
});
上面的 stats 存储了一些文件的信息:

3. 文件的写入
有文件的读取,就会有写入
- 文件的读取:
fs.readFile(path[,options],callback) - 文件的写入:
fs.writeFile(file,data[,options],callback)
const fs = require("fs");
let content = "aaaa";
fs.writeFile("./foo.txt", content, { encoding: "utf-8", flag: "a+" }, (err) => {
console.log(err);
});
在上面的代码中,你会发现有一个对象类型 options,这个就是写入时填写的 option 参数:
flag:写入的方式encoding:字符编码
3.1 flag 选项
flag 的值有很多,详见文档,这里介绍几种常见的:
w打开文件写入(默认)w+打开文件进行读写(可读可写),如果不存在则创建文件r打开文件进行读取,读取时的默认值r+打开文件进行读写,如果不存在那么抛出异常a打开要写入的文件,将流放在文件末尾。如果不存在则创建文件;a+打开文件进行读写(可读可写),将流放在文件末尾。如果不存在则创建文件
3.2 encoding 选项
常见的字符编码有:
ASCII编码:ASCII码占用一个字节(8位),一共可以表示256个字符。GBXXXX编码:为了显示中文设计的一套编码规则(著名的GB2312)GBK编码:相比于GB2312,多收录了包括古汉语、繁体、日语朝鲜语等汉字,在中文版Windows操作系统下, 通常使用的编码方式都是GBK编码Unicode:Unicode(统一码、万国码、单一码、标准万国码)是业界的一种标准,它可以使电脑得以体现世界上数十种文字的系统。UTF-8/-16/-32:Unicode和它们的关系可以看成Unicode是字符集,UTF-32/UTF-16/UTF-8是三种字符编码方案UTF-32:UTF-32又称UCS-4是一种将Unicode字符编码的协定,对每个字符都使用4字节。就空间而言,是效率非常低的,因此UTF-32使用并不广泛UTF-16:尽管有Unicode字符非常多,但是实际上大多数人不会用到超过前65535个之外的字,因此就有了UTF-16(因为16位 = 2字节)UTF-8:UTF-8(8-bit Unicode Transformation Format)是一种针对[Unicode]的可变长度字符编码,它使用一至四个字节为每个字符编码
4. 文件夹的操作
文件夹的操作都比较简单
4.1 mkdir 创建文件夹
文件夹的创建可以通过 mkdir 或 mkdirSync:
const fs = require("fs");
fs.mkdir("./coder/aaa", (err) => {
console.log(err);
});
4.2 readdir 读取文件夹
- 文件夹的读取可以通过
readdir或readdirSync
fs.readdir("./coder", (err, files) => {
console.log(err, files);
});
上面这段代码将会打印出 coder 文件夹下所有的 文件和文件夹的名称

- 如果想拿一下文件的类型,可以配置
option的withFileTypes为true
fs.readdir("./coder", { withFileTypes: true }, (err, files) => {
console.log(err, files);
files.forEach((file) => {
// isDirectory 判断是一个文件夹
console.log(file.isDirectory());
});
});

- 有了这个选项,我们就可以拿出所有文件夹下面的文件了。
function getAllFile(path) {
const ret = [];
function readDirectory(path) {
const files = fs.readdirSync(path, { withFileTypes: true });
files.forEach((file) => {
// isDirectory 判断是否是一个文件夹
if (file.isDirectory()) {
readDirectory(`${path}/${file.name}`);
} else {
ret.push(file);
}
});
}
readDirectory(path);
return ret;
}
console.log(getAllFile("./coder"));
创建以下结构的文件夹测试:


可以看到成功我们已经将所有文件都筛选了出来。
4.3 rename 文件夹重命名
fs.rename("./coder", "./coderyjw", (err) => {
if (err) {
console.log("重命名失败", err);
}
});
rename 这个方法不仅可以重命名文件夹,也可以重命名文件