原文来自 我的个人博客
前言
对于任何一个为服务端服务的语言或者框架来说,通常都会有自己的文件系统:
- 因为服务器需要将各种数据、文件等放置到不同的地方;
- 比如用户数据可能大多数都是放到数据库的;
- 比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在操作系统上的
而 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
这个方法不仅可以重命名文件夹,也可以重命名文件