原文出自:https://www.pandashen.comjavascript
本文全部代码git地址:https://gitee.com/vr2/node/tree/master/fsjava
fs
核心模块来实现的,包括文件目录的建立、删除、查询以及文件的读取和写入,在
fs
模块中,全部的方法都分为同步和异步两种实现,具备
sync
后缀的方法为同步方法,不具备
sync
后缀的方法为异步方法,在了解文件操做的方法以前有一些关于系统和文件的前置知识,如文件的权限位
mode
、标识位
flag
、文件描述符
fd
等,因此在了解
fs
方法的以前会先将这几个概念明确。
由于 fs
模块须要对文件进行操做,会涉及到操做权限的问题,因此须要先清楚文件权限是什么,都有哪些权限。node
文件权限表:git
权限分配 | 文件全部者 | 文件所属组 | 其余用户 | ||||||
---|---|---|---|---|---|---|---|---|---|
权限项 | 读 | 写 | 执行 | 读 | 写 | 执行 | 读 | 写 | 执行 |
字符表示 | 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
。bash
r | w | — | r | — | — | r | — | — |
---|---|---|---|---|---|---|---|---|
4 | 2 | 0 | 4 | 0 | 0 | 4 | 0 | 0 |
6 | 4 | 4 |
NodeJS 中,标识位表明着对文件的操做方式,如可读、可写、便可读又可写等等,在下面用一张表来表示文件操做的标识位和其对应的含义。app
符号 | 含义 |
---|---|
r | 读取文件,若是文件不存在则抛出异常。 |
r+ | 读取并写入文件,若是文件不存在则抛出异常。 |
rs | 读取并写入文件,指示操做系统绕开本地文件系统缓存。 |
w | 写入文件,文件不存在会被建立,存在则清空后写入。 |
wx | 写入文件,排它方式打开。 |
w+ | 读取并写入文件,文件不存在则建立文件,存在则清空后写入。 |
wx+ | 和 w+ 相似,排他方式打开。 |
a | 追加写入,文件不存在则建立文件。 |
ax | 与 a 相似,排他方式打开。 |
a+ | 读取并追加写入,不存在则建立。 |
ax+ | 与 a+ 相似,排他方式打开。 |
上面表格就是这些标识位的具体字符和含义,可是 flag
是不常用的,不容易被记住,因此在下面总结了一个加速记忆的方法。异步
r+
和 w+
的区别,当文件不存在时,r+
不会建立文件,而会抛出异常,但 w+
会建立文件;若是文件存在,r+
不会自动清空文件,但 w+
会自动把已有文件的内容清空。
操做系统会为每一个打开的文件分配一个名为文件描述符的数值标识,文件操做使用这些文件描述符来识别与追踪每一个特定的文件,Window 系统使用了一个不一样但概念相似的机制来追踪资源,为方便用户,NodeJS 抽象了不一样操做系统间的差别,为全部打开的文件分配了数值的文件描述符。
在 NodeJS 中,每操做一个文件,文件描述符是递增的,文件描述符通常从 3
开始,由于前面有 0
、1
、2
三个比较特殊的描述符,分别表明 process.stdin
(标准输入)、process.stdout
(标准输出)和 process.stderr
(错误输出)。
文件操做中的基本方法都是对文件进行总体操做,即整个文件数据直接放在内存中操做,如读取、写入、拷贝和追加,因为计算机的内存容量有限,对文件操做须要考虑性能,因此这些方法只针对操做占用内存较小的文件。
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 ...
异步读取方法 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 });
writeFileSync
有三个参数:
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
异步写入方法 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 }); } });
appendFileSync
有三个参数:
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
异步追加写入方法 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 }); } });
同步拷贝写入方法 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
异步拷贝写入方法 copyFile
和 copyFileSync
前两个参数相同,最后一个参数为回调函数,在拷贝完成后执行。
const fs = require("fs"); fs.copyFile("3.txt", "4.txt", () => { fs.readFile("4.txt", "utf8", (err, data) => { console.log(data); // Hello world }); });
open
方法有四个参数:
0o666
;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
close
方法有两个参数,第一个参数为关闭文件的文件描述符 fd
,第二参数为回调函数,回调函数有一个参数 err
(错误),关闭文件后执行。
const fs = require("fs"); fs.open("4.txt", "r", (err, fd) => { fs.close(fd, err => { console.log("关闭成功"); }); }); // 关闭成功
read
方法与 readFile
不一样,通常针对于文件太大,没法一次性读取所有内容到缓存中或文件大小未知的状况,都是屡次读取到 Buffer 中。
想了解 Buffer 能够看 NodeJS —— Buffer 解读。
read
方法中有六个参数:
open
打开;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> // 你好
fsync
方法有两个参数,第一个参数为文件描述符 fd
,第二个参数为回调函数,回调函数中有一个参数 err
(错误),在同步磁盘缓存后执行。
在使用 write
方法向文件写入数据时,因为不是一次性写入,因此最后一次写入在关闭文件以前应先同步磁盘缓存,fsync
方法将在后面配合 write
一块儿使用。
write
方法与 writeFile
不一样,是将 Buffer 中的数据写入文件,Buffer 的做用是一个数据中转站,可能数据的源占用内存太大或内存不肯定,没法一次性放入内存中写入,因此分段写入,多与 read
方法配合。
write
方法中有六个参数:
open
打开;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
的 “你” 字以后,可是最后的 “好” 并无被保留,说明先清空了文件中 “你” 字以后的内容再写入。
以前咱们使用 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
,也分为同步和异步两种实现。
accessSync
方法传入一个目录的路径,检查传入路径下的目录是否可读可写,当有操做权限的时候没有返回值,没有权限或路径非法时抛出一个 Error
对象,因此使用时多用 try...catch...
进行异常捕获。
const fs = require("fs"); try { fs.accessSync("a/b/c"); console.log("可读可写"); } catch (err) { console.error("不可访问"); }
access
方法与第一个参数为一个目录的路径,最后一个参数为一个回调函数,回调函数有一个参数为 err
(错误),在权限检测后触发,若是有权限 err
为 null
,没有权限或路径非法 err
是一个 Error
对象。
const fs = require("fs"); fs.access("a/b/c", err => { if (err) { console.error("不可访问"); } else { console.log("可读可写"); } });
文件目录的 Stats
对象存储着关于这个文件或文件夹的一些重要信息,如建立时间、最后一次访问的时间、最后一次修改的时间、文章所占字节和判断文件类型的多个方法等等。
statSync
方法参数为一个目录的路径,返回值为当前目录路径的 Stats
对象,如今经过 Stats
对象获取 a
目录下的 b
目录下的 c.txt
文件的字节大小,文件内容为 “你好”。
const fs = require("fs"); let statObj = fs.statSync("a/b/c.txt"); console.log(statObj.size); // 6
stat
方法的第一个参数为目录的路径,最后一个参数为回调函数,回调函数有两个参数 err
(错误)和 Stats
对象,在读取 Stats
后执行,一样实现上面的读取文件字节数的例子。
const fs = require("fs"); fs.stat("a/b/c.txt", (err, statObj) => { console.log(statObj.size); });
// 6
mkdirSync
方法参数为一个目录的路径,没有返回值,在建立目录的过程当中,必须保证传入的路径前面的文件目录都存在,不然会抛出异常。
const fs = require("fs"); // 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdirSync("a/b/c")
mkdir
方法的第一个参数为目录的路径,最后一个参数为回调函数,回调函数有一个参数 err
(错误),在执行建立操做后执行,一样须要路径前部分的文件夹都存在。
const fs = require("fs"); // 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdir("a/b/c", err => { if (!err) console.log("建立成功"); }); // 建立成功
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' ]
readdir
方法的前两个参数与 readdirSync
相同,第三个参数为一个回调函数,回调函数有两个参数 err
(错误)和 data
(存储文件目录中成员名称的数组),在读取文件目录后执行。
上面案例异步的写法:
const fs = require("fs"); fs.readdir("a/b", (err, data) => { if (!err) console.log(data); }); // [ 'c', 'index.js' ]
不管同步仍是异步,删除文件目录时必须保证文件目录的路径存在,且被删除的文件目录为空,即不存在任何文件夹和文件。
rmdirSync
的参数为要删除目录的路径,如今存在 a
目录和 a
目录下的 b
目录,删除 b
目录。
const fs = require("fs"); fs.rmdirSync("a/b");
rmdir
方法的第一个参数与 rmdirSync
相同,最后一个参数为回调函数,函数中存在一个参数 err
(错误),在删除目录操做后执行。
const fs = require("fs"); fs.rmdir("a/b", err => { if (!err) console.log("删除成功"); }); // 删除成功
unlinkSync
的参数为要删除文件的路径,如今存在 a
目录和 a
目录下的 index.js
文件,删除 index.js
文件。
const fs = require("fs"); fs.unlinkSync("a/inde.js");
unlink
方法的第一个参数与 unlinkSync
相同,最后一个参数为回调函数,函数中存在一个参数 err
(错误),在删除文件操做后执行。
const fs = require("fs"); fs.unlink("a/index.js", err => { if (!err) console.log("删除成功"); }); // 删除成功
在 fs
全部模块都有同步异步两种实现,同步方法的特色就是阻塞代码,致使性能差,异步代码的特色就是回调函数嵌套多,在使用 fs
应尽可能使用异步方式编程来保证性能,若是以为回调函数嵌套很差维护,可使用 Promise 和 async/await
的方式解决。