原文出自:https://www.pandashen.com
本文所有代码git地址:https://gitee.com/vr2/node/tree/master/fs
fs 概述
fs
核心模块来实现的,包括文件目录的创建、删除、查询以及文件的读取和写入,在 fs
模块中,所有的方法都分为同步和异步两种实现,具有 sync
后缀的方法为同步方法,不具有 sync
后缀的方法为异步方法,在了解文件操作的方法之前有一些关于系统和文件的前置知识,如文件的权限位 mode
、标识位 flag
、文件描述符 fd
等,所以在了解 fs
方法的之前会先将这几个概念明确。权限位 mode
因为 fs
模块需要对文件进行操作,会涉及到操作权限的问题,所以需要先清楚文件权限是什么,都有哪些权限。
文件权限表:
权限分配 | 文件所有者 | 文件所属组 | 其他用户 | ||||||
---|---|---|---|---|---|---|---|---|---|
权限项 | 读 | 写 | 执行 | 读 | 写 | 执行 | 读 | 写 | 执行 |
字符表示 | r | w | x | r | w | x | r | w | x |
数字表示 | 4 | 2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
在上面表格中,我们可以看出系统中针对三种类型进行权限分配,即文件所有者(自己)、文件所属组(家人)和其他用户(陌生人),文件操作权限又分为三种,读、写和执行,数字表示为八进制数,具备权限的八进制数分别为 4
、2
、1
,不具备权限为 0
。
Git
,使用 Linux 命令 ls -al
来查目录中文件和文件夹的权限位,如果对 Git
和 Linux
命令不熟悉,可以看 Git 命令总结,从零到熟悉(全)。drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 core
-rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md
上面的目录信息当中,很容易看出用户名、创建时间和文件名等信息,但最重要的是开头第一项(十位的字符)。
第一位代表是文件还是文件夹,d
开头代表文件夹,-
开头的代表文件,而后面九位就代表当前用户、用户所属组和其他用户的权限位,按每三位划分,分别代表读(r)、写(w)和执行(x),-
代表没有当前位对应的权限。
权限参数 mode
主要针对 Linux 和 Unix 操作系统,Window 的权限默认是可读、可写、不可执行,所以权限位数字表示为 0o666
,转换十进制表示为 438
。
r | w | — | r | — | — | r | — | — |
---|---|---|---|---|---|---|---|---|
4 | 2 | 0 | 4 | 0 | 0 | 4 | 0 | 0 |
6 | 4 | 4 |
标识位 flag
NodeJS 中,标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,在下面用一张表来表示文件操作的标识位和其对应的含义。
符号 | 含义 |
---|---|
r | 读取文件,如果文件不存在则抛出异常。 |
r+ | 读取并写入文件,如果文件不存在则抛出异常。 |
rs | 读取并写入文件,指示操作系统绕开本地文件系统缓存。 |
w | 写入文件,文件不存在会被创建,存在则清空后写入。 |
wx | 写入文件,排它方式打开。 |
w+ | 读取并写入文件,文件不存在则创建文件,存在则清空后写入。 |
wx+ | 和 w+ 类似,排他方式打开。 |
a | 追加写入,文件不存在则创建文件。 |
ax | 与 a 类似,排他方式打开。 |
a+ | 读取并追加写入,不存在则创建。 |
ax+ | 与 a+ 类似,排他方式打开。 |
上面表格就是这些标识位的具体字符和含义,但是 flag
是不经常使用的,不容易被记住,所以在下面总结了一个加速记忆的方法。
- r:读取
- w:写入
- s:同步
- +:增加相反操作
- x:排他方式
r+
和 w+
的区别,当文件不存在时,r+
不会创建文件,而会抛出异常,但 w+
会创建文件;如果文件存在,r+
不会自动清空文件,但 w+
会自动把已有文件的内容清空。
文件描述符 fd
操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件,Window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,NodeJS 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符。
在 NodeJS 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3
开始,因为前面有 0
、1
、2
三个比较特殊的描述符,分别代表 process.stdin
(标准输入)、process.stdout
(标准输出)和 process.stderr
(错误输出)。
文件操作的基本方法
文件操作中的基本方法都是对文件进行整体操作,即整个文件数据直接放在内存中操作,如读取、写入、拷贝和追加,由于计算机的内存容量有限,对文件操作需要考虑性能,所以这些方法只针对操作占用内存较小的文件。
1、文件读取
(1) 同步读取方法 readFileSync
readFileSync
有两个参数:
- 第一个参数为读取文件的路径或文件描述符;
- 第二个参数为
options
,默认值为null
,其中有encoding
(编码,默认为null
)和flag
(标识位,默认为r
),也可直接传入encoding
; - 返回值为文件的内容,如果没有
encoding
,返回的文件内容为 Buffer,如果有按照传入的编码解析。
若现在有一个文件名为 1.txt
,内容为 “Hello”,现在使用 readFileSync
读取。
const fs = require("fs"); let buf = fs.readFileSync("1.txt"); let data = fs.readFileSync("1.txt", "utf8"); console.log(buf); // <Buffer 48 65 6c 6c 6f> console.log(data); // Hello
注意:同步读取不存在的文件的时候可以使用try catch. try catch只能同步捕获异常
const fs = require('fs') // 当前目录下并不存在txt.txt文件 try{ fs.readFileSync('./txt.txt') }catch(e) { console.log(e) } //{ Error: ENOENT: no such file or directory, open './txt.txt' // at Object.openSync ...
(2) 异步读取方法 readFile
异步读取方法 readFile
与 readFileSync
的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 err
(错误)和 data
(数据),该方法没有返回值,回调函数在读取文件成功后执行。
依然读取 1.txt
文件:
const fs = require("fs"); fs.readFile("1.txt", "utf8", (err, data) => { console.log(err); // null console.log(data); // Hello });
2、文件写入
(1) 同步写入方法 writeFileSync
writeFileSync
有三个参数:
- 第一个参数为写入文件的路径或文件描述符;
- 第二个参数为写入的数据,类型为 String 或 Buffer;
- 第三个参数为
options
,默认值为null
,其中有encoding
(编码,默认为utf8
)、flag
(标识位,默认为w
)和mode
(权限位,默认为0o666
),也可直接传入encoding
。
若现在有一个文件名为 2.txt
,内容为 “12345”,现在使用 writeFileSync
写入。
const fs = require("fs"); fs.writeFileSync("2.txt", "Hello world"); let data = fs.readFileSync("2.txt", "utf8"); console.log(data); // Hello world
(2) 异步写入方法 writeFile
异步写入方法 writeFile
与 writeFileSync
的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err
(错误),回调函数在文件写入数据成功后执行。
const fs = require("fs"); fs.writeFile("2.txt", "Hello world", err => { if (!err) { fs.readFile("2.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });
3、文件追加写入
(1) 同步追加写入方法 appendFileSync
appendFileSync
有三个参数:
- 第一个参数为写入文件的路径或文件描述符;
- 第二个参数为写入的数据,类型为 String 或 Buffer;
- 第三个参数为
options
,默认值为null
,其中有encoding
(编码,默认为utf8
)、flag
(标识位,默认为a
)和mode
(权限位,默认为0o666
),也可直接传入encoding
。
若现在有一个文件名为 3.txt
,内容为 “Hello”,现在使用 appendFileSync
追加写入 “ world”。
const fs = require("fs"); fs.appendFileSync("3.txt", " world"); let data = fs.readFileSync("3.txt", "utf8"); console.log(data); // Hello world
(2) 异步追加写入方法 appendFile
异步追加写入方法 appendFile
与 appendFileSync
的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err
(错误),回调函数在文件追加写入数据成功后执行。
const fs = require("fs"); fs.appendFile("3.txt", " world", err => { if (!err) { fs.readFile("3.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });
4、文件拷贝写入
(1) 同步拷贝写入方法 copyFileSync
同步拷贝写入方法 copyFileSync
有两个参数,第一个参数为被拷贝的源文件路径,第二个参数为拷贝到的目标文件路径,如果目标文件不存在,则会创建并拷贝。
现在将上面 3.txt
的内容拷贝到 4.txt
中:
const fs = require("fs"); fs.copyFileSync("3.txt", "4.txt"); let data = fs.readFileSync("4.txt", "utf8"); console.log(data); // Hello world
(2) 异步拷贝写入方法 copyFile
异步拷贝写入方法 copyFile
和 copyFileSync
前两个参数相同,最后一个参数为回调函数,在拷贝完成后执行。
const fs = require("fs"); fs.copyFile("3.txt", "4.txt", () => { fs.readFile("4.txt", "utf8", (err, data) => { console.log(data); // Hello world }); });
文件操作的高级方法
1、打开文件 open
open
方法有四个参数:
- path:文件的路径;
- flag:标识位;
- mode:权限位,默认
0o666
; - callback:回调函数,有两个参数
err
(错误)和fd
(文件描述符),打开文件后执行。
const fs = require("fs"); fs.open("4.txt", "r", (err, fd) => { console.log(fd); fs.open("5.txt", "r", (err, fd) => { console.log(fd); }); }); // 3 // 4
2、关闭文件 close
close
方法有两个参数,第一个参数为关闭文件的文件描述符 fd
,第二参数为回调函数,回调函数有一个参数 err
(错误),关闭文件后执行。
const fs = require("fs"); fs.open("4.txt", "r", (err, fd) => { fs.close(fd, err => { console.log("关闭成功"); }); }); // 关闭成功
3、读取文件 read
read
方法与 readFile
不同,一般针对于文件太大,无法一次性读取全部内容到缓存中或文件大小未知的情况,都是多次读取到 Buffer 中。
想了解 Buffer 可以看 NodeJS —— Buffer 解读。
read
方法中有六个参数:
- fd:文件描述符,需要先使用
open
打开; - buffer:要将内容读取到的 Buffer;
- offset:整数,向 Buffer 写入的初始位置;
- length:整数,读取文件的长度;
- position:整数,读取文件初始位置;
- callback:回调函数,有三个参数
err
(错误),bytesRead
(实际读取的字节数),buffer
(被写入的缓存区对象),读取执行完成后执行。
下面读取一个 6.txt
文件,内容为 “你好”。
const fs = require("fs"); let buf = Buffer.alloc(6); // 打开文件 fs.open("6.txt", "r", (err, fd) => { // 读取文件 fs.read(fd, buf, 0, 3, 0, (err, bytesRead, buffer) => { console.log(bytesRead); console.log(buffer); // 继续读取 fs.read(fd, buf, 3, 3, 3, (err, bytesRead, buffer) => { console.log(bytesRead); console.log(buffer); console.log(buffer.toString()); }); }); }); // 3 // <Buffer e4 bd a0 00 00 00> // 3 // <Buffer e4 bd a0 e5 a5 bd> // 你好
4、同步磁盘缓存 fsync
fsync
方法有两个参数,第一个参数为文件描述符 fd
,第二个参数为回调函数,回调函数中有一个参数 err
(错误),在同步磁盘缓存后执行。
在使用 write
方法向文件写入数据时,由于不是一次性写入,所以最后一次写入在关闭文件之前应先同步磁盘缓存,fsync
方法将在后面配合 write
一起使用。
5、写入文件 write
write
方法与 writeFile
不同,是将 Buffer 中的数据写入文件,Buffer 的作用是一个数据中转站,可能数据的源占用内存太大或内存不确定,无法一次性放入内存中写入,所以分段写入,多与 read
方法配合。
write
方法中有六个参数:
- fd:文件描述符,需要先使用
open
打开; - buffer:存储将要写入文件数据的 Buffer;
- offset:整数,从 Buffer 读取数据的初始位置;
- length:整数,读取 Buffer 数据的字节数;
- position:整数,写入文件初始位置;
- callback:回调函数,有三个参数
err
(错误),bytesWritten
(实际写入的字节数),buffer
(被读取的缓存区对象),写入完成后执行。
下面将一个 Buffer 中间的两个字写入文件 6.txt
,原内容为 “你好”。
const fs = require("fs"); let buf = Buffer.from("你还好吗"); // 打开文件 fs.open("6.txt", "r+", (err, fd) => { // 读取 buf 向文件写入数据 fs.write(fd, buf, 3, 6, 3, (err, bytesWritten, buffer) => { // 同步磁盘缓存 fs.fsync(fd, err => { // 关闭文件 fs.close(fd, err => { console.log("关闭文件"); }); }); }); }); // 这里为了看是否写入成功简单粗暴的使用 readFile 方法 fs.readFile("6.txt", "utf8", (err, data) => { console.log(data); }); // 你还好
上面代码将 “你还好吗” 中间的 “还好” 从 Buffer 中读取出来写入到 6.txt
的 “你” 字之后,但是最后的 “好” 并没有被保留,说明先清空了文件中 “你” 字之后的内容再写入。
6、针对大文件实现 copy
之前我们使用 readFile
和 writeFile
实现了一个 copy
函数,那个 copy
函数是将被拷贝文件的数据一次性读取到内存,一次性写入到目标文件中,针对小文件。
如果是一个大文件一次性写入不现实,所以需要多次读取多次写入,接下来使用上面的这些方法针对大文件和文件大小未知的情况实现一个 copy
函数。
大文件拷贝
// copy 方法 function copy(src, dest, size = 16 * 1024, callback) { // 打开源文件 fs.open(src, "r", (err, readFd) => { // 打开目标文件 fs.open(dest, "w", (err, writeFd) => { let buf = Buffer.alloc(size); let readed = 0; // 下次读取文件的位置 let writed = 0; // 下次写入文件的位置 (function next() { // 读取 fs.read(readFd, buf, 0, size, readed, (err, bytesRead) => { readed += bytesRead; // 如果都不到内容关闭文件 if(!bytesRead) fs.close(readFd, err => console.log("关闭源文件")); // 写入 fs.write(writeFd, buf, 0, bytesRead, writed, (err, bytesWritten) => { // 如果没有内容了同步缓存,并关闭文件后执行回调 if (!bytesWritten) { fs.fsync(writeFd, err => { fs.close(writeFd, err => return !err && callback()); }); } writed += bytesWritten; // 继续读取、写入 next(); } ); }); })(); }); }); }
在上面的 copy
方法中,我们手动维护的下次读取位置和下次写入位置,如果参数 readed
和 writed
的位置传入 null
,NodeJS 会自动帮我们维护这两个值。
现在有一个文件 6.txt
内容为 “你好”,一个空文件 7.txt
,我们将 6.txt
的内容写入 7.txt
中。
const fs = require("fs"); // buffer 的长度 const BUFFER_SIZE = 3; // 拷贝文件内容并写入 copy("6.txt", "7.txt", BUFFER_SIZE, () => { fs.readFile("7.txt", "utf8", (err, data) => { // 拷贝完读取 7.txt 的内容 console.log(data); // 你好 }); });
在 NodeJS 中进行文件操作,多次读取和写入时,一般一次读取数据大小为 64k
,写入数据大小为 16k
。
文件目录操作方法
下面的这些操作文件目录的方法有一个共同点,就是传入的第一个参数都为文件的路径,如:a/b/c/d
,也分为同步和异步两种实现。
1、查看文件目录操作权限
(1) 同步查看操作权限方法 accessSync
accessSync
方法传入一个目录的路径,检查传入路径下的目录是否可读可写,当有操作权限的时候没有返回值,没有权限或路径非法时抛出一个 Error
对象,所以使用时多用 try...catch...
进行异常捕获。
const fs = require("fs"); try { fs.accessSync("a/b/c"); console.log("可读可写"); } catch (err) { console.error("不可访问"); }
(2) 异步查看操作权限方法 access
access
方法与第一个参数为一个目录的路径,最后一个参数为一个回调函数,回调函数有一个参数为 err
(错误),在权限检测后触发,如果有权限 err
为 null
,没有权限或路径非法 err
是一个 Error
对象。
const fs = require("fs"); fs.access("a/b/c", err => { if (err) { console.error("不可访问"); } else { console.log("可读可写"); } });
2、获取文件目录的 Stats 对象
文件目录的 Stats
对象存储着关于这个文件或文件夹的一些重要信息,如创建时间、最后一次访问的时间、最后一次修改的时间、文章所占字节和判断文件类型的多个方法等等。
(1) 同步获取 Stats 对象方法 statSync
statSync
方法参数为一个目录的路径,返回值为当前目录路径的 Stats
对象,现在通过 Stats
对象获取 a
目录下的 b
目录下的 c.txt
文件的字节大小,文件内容为 “你好”。
const fs = require("fs"); let statObj = fs.statSync("a/b/c.txt"); console.log(statObj.size); // 6
(2) 异步获取 Stats 对象方法 stat
stat
方法的第一个参数为目录的路径,最后一个参数为回调函数,回调函数有两个参数 err
(错误)和 Stats
对象,在读取 Stats
后执行,同样实现上面的读取文件字节数的例子。
const fs = require("fs"); fs.stat("a/b/c.txt", (err, statObj) => { console.log(statObj.size); });
// 6
3、创建文件目录
(1) 同步创建目录方法 mkdirSync
mkdirSync
方法参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常。
const fs = require("fs"); // 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdirSync("a/b/c")
(2) 异步创建目录方法 mkdir
mkdir
方法的第一个参数为目录的路径,最后一个参数为回调函数,回调函数有一个参数 err
(错误),在执行创建操作后执行,同样需要路径前部分的文件夹都存在。
const fs = require("fs"); // 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdir("a/b/c", err => { if (!err) console.log("创建成功"); }); // 创建成功
4、读取文件目录
(1) 同步读取目录方法 readdirSync
readdirSync
方法有两个参数:
- 第一个参数为目录的路径,传入的路径前部分的目录必须存在,否则会报错;
- 第二个参数为
options
,其中有encoding
(编码,默认值为utf8
),也可直接传入encoding
; - 返回值为一个存储文件目录中成员名称的数组。
假设现在已经存在了 a
目录和 a
下的 b
目录,b
目录中有 c
目录和 index.js
文件,下面读取文件目录结构。
const fs = require("fs"); let data = fs.readdirSync("a/b"); console.log(data); // [ 'c', 'index.js' ]
(2) 异步读取目录方法 readdir
readdir
方法的前两个参数与 readdirSync
相同,第三个参数为一个回调函数,回调函数有两个参数 err
(错误)和 data
(存储文件目录中成员名称的数组),在读取文件目录后执行。
上面案例异步的写法:
const fs = require("fs"); fs.readdir("a/b", (err, data) => { if (!err) console.log(data); }); // [ 'c', 'index.js' ]
5、删除文件目录
无论同步还是异步,删除文件目录时必须保证文件目录的路径存在,且被删除的文件目录为空,即不存在任何文件夹和文件。
(1) 同步删除目录方法 rmdirSync
rmdirSync
的参数为要删除目录的路径,现在存在 a
目录和 a
目录下的 b
目录,删除 b
目录。
const fs = require("fs"); fs.rmdirSync("a/b");
(2) 异步删除目录方法 rmdir
rmdir
方法的第一个参数与 rmdirSync
相同,最后一个参数为回调函数,函数中存在一个参数 err
(错误),在删除目录操作后执行。
const fs = require("fs"); fs.rmdir("a/b", err => { if (!err) console.log("删除成功"); }); // 删除成功
6、删除文件操作
(1) 同步删除文件方法 unlinkSync
unlinkSync
的参数为要删除文件的路径,现在存在 a
目录和 a
目录下的 index.js
文件,删除 index.js
文件。
const fs = require("fs"); fs.unlinkSync("a/inde.js");
(2) 异步删除文件方法 unlink
unlink
方法的第一个参数与 unlinkSync
相同,最后一个参数为回调函数,函数中存在一个参数 err
(错误),在删除文件操作后执行。
const fs = require("fs"); fs.unlink("a/index.js", err => { if (!err) console.log("删除成功"); }); // 删除成功
总结
在 fs
所有模块都有同步异步两种实现,同步方法的特点就是阻塞代码,导致性能差,异步代码的特点就是回调函数嵌套多,在使用 fs
应尽量使用异步方式编程来保证性能,如果觉得回调函数嵌套不好维护,可以使用 Promise 和 async/await
的方式解决。