Node.js fs模块,全称是file system模块,即文件系统模块,顾名思义,这个模块是用来操作文件。而我们常用的文件操作有:打开文件,关闭文件,读取文件数据,写入数据到文件,创建文件夹(目录),删除文件夹(目录),移动文件到文件夹(目录)中。
但是在讨论文件操作之前,我们必须要了解一些文件系统的基本知识。
其实这个问题不能算是问题,无论是windows系统,还是linux系统,进入之后,我们最常操作的就是文件,比如打开一个txt文本文件,打开一个jpg图片文件,打开一个exe可执行文件,这里其实想说的是文件夹(目录),也算是文件。在文件系统中,文件夹(目录)也算是文件的一种。
使用过linux系统的小伙伴一定对文件权限位很熟悉,而常使用windows系统的小伙伴可能就它不感冒了。其实无论是linux系统还是,windows系统的文件系统都有文件权限位。
文件权限位就是用来说明文件的权限的,即谁可以对文件进行哪些操作。
其中“谁”可以是: 文件所有者,文件所属组,其他用户
“哪些操作”可以是:读,写,执行
每个文件都具备:文件所有者,文件所属组,其他用户 三个用户
每个用户都具备:读,写,执行 三种权限
权限 | 权限位 | 权限值 |
可读 | 100 | 4 |
可写 | 010 | 2 |
可执行 | 001 | 1 |
所以最大权限值是 7(可读可写可执行),最小权限值是 0(不可读不可写不可执行)
我们可以使用命令窗口输入命令 ls -al,来查看文件的权限
我们选取两个代表,分别是文件夹的权限,文件的权限
所以一个文件完整的权限值 在 000 ~ 777 之间
在文件系统之中,我们可以打开多个文件,那么文件系统如何识别多个文件呢?
通过文件名?那肯定存在文件同名的情况
通过文件内容hash值?那肯定存在拷贝文件,即hash值相同的文件
所以为了能够唯一定位到打开的某个文件,文件系统需要使用文件描述符去描述一个唯一的文件,并且可以根据我呢见描述符找到对应的文件。
windows,linux系统的文件系统都有这套机制,而Node.js 自身的fs模块也有这样一套机制,Node.js 的文件描述符很简单,即第一个打开的文件就标记位0,第二个打开的文件就标记为1,当文件关闭,则回收其文件标识符,方便后续循环利用。
有时候,我们打开一个文件只是为了读取其中的内容,有时候我们打开一个文件是为了修改其中的内容,有时候,我们打开一个文件是为了读取和写入内容,所以如果文件系统知道了我们打开文件的目的是什么,就可以提前准备。所以文件系统在我们打开某个文件前,需要我们给出文件操作符,即我们打开文件的目的是什么。常见的文件操作符有 r (读), w (写),r+(读写),w+(读写)。
那么为什么我们需要告知文件系统我们打开文件的目的呢?
比如,我们读取一个文件的前提是要有这个文件,如果我们读取的文件不存在,则文件系统需要给我们报错。
而我们写入数据到一个文件,则该文件不需要一定是存在的,可以不存在,即当我们写入数据到文件完成后,再创建文件也可以。
所以同样是文件不存在,但是不同的文件操作对应不同的结果。这就是我们需要提供文件操作符给文件系统的原因。
通过上面的基础知识了解,我们发现要操作一个文件,前提必然是打开文件,而打开文件时,需要注意很多东西,比如打开文件会得到一个文件描述符,打开文件前需要指定文件操作符,以及权限位。所以我们来看看fs模块提供的打开文件的操作。
fs.open(path, flags, mode, callback)参数含义如下
path | 要打开的文件所在的路径,最好是绝对路径,相对路径会存在问题 |
flags | 文件操作符,默认为'r' |
mode | 文件权限位,默认为 0o666,即所有用户可读可写 |
callback | 异步打开文件后回调函数,回调函数的参数有 err : 打开文件遇到的异常, fd 文件描述符 |
这里我们对输入的实参值逐一解释下:
path.resolve('text.txt') 会得到一个绝对路径
虽然fs所有方法都支持相对路径,但是我们不提倡使用相对路径,因为相对路径会产生意外的错误,我们印象中的相对路径应该是当前文件test.js所在文件夹的路径,但是实际上相对路径是node命令执行时所在路径
第一个node执行时,所在路径是D盘根目录,此时node执行结果为undefined,因为fs.open的文件路径是 D:\test.txt,因为fs.open相对路径使用的是node命令执行所在目录。
第二个node执行时,所在路径是D:\Desktop\test目录,刚好是test.js所在目录,也是test.txt所在目录,所以可以成功打开文件。
第二个参数是文件操作符,即打开文件后的操作是啥?是为了读取文件数据,还是写入数据到文件,还是读写,还是写读?默认值是 读取文件数据,即'r'。
常见的文件操作符,及其作用如下
第三个参数是设置文件权限位,这个其实就有点耍流氓了,我创建好一个文件之后,必然就已经指定好了文件的权限位,你这可好,直接选择忽视设置好的权限位,直接在打开文件前给你改了。只能说真是666啊。这里还有一个点要说一下,就是默认值0o666,对应的就是三个用户都是可读可写权限,这里为啥使用八进制呢?因为权限位就是三位二进制数,比如可读100,可写010,可执行001,而三位二进制数就对应一位八进制数。
第四个参数是一个callback回调函数,因为当前fs.open是一个异步操作,即打开文件的动作是异步的,晚于同步代码执行,所以打开文件后的回调就是异步回调,当打开文件的I/O操作完成后,该回调函数会被推入到poll队列中,等待事件循环取出后执行。
另外有一点需要注意,Node.js中提倡异步回调函数的形参设计都是错误优先的,即err形参放在第一个,其他形参放在第二个及之后。而Node.js内置的模块的方法的异步回调函数都是错误优先的。
还有,callback回调函数除了错误优先的err参数外,还有一个fd形参,即file descriptor,文件描述符,Node.js的文件系统中打开第一个文件fd为0,打开第二个文件fd为1,打开第三个文件fd为2,打开第四个文件fd为4,以此类推。
那么这里fs.open明明是打开的第一个文件,为啥fd是3呢?
因为我们这里的fs.open打开的不是第一个文件,Node.js在启动过程中会打开三个文件流,分别是 标准输入,标准输出,标准错误,且是长期打开的,只有Node应用关闭后,才释放
所以我们使用fs.open打开文件,必然只能是Node文件系统中第四个打开的文件,所以对应fd为3。
而fs.open操作最重要的作用就是获取fd,方便后续文件读写操作时,根据fd找到对应的文件。
在这里我们其实可以思考一个问题,如何将callback中的fd暴露出去,即暴露到callback外面,例如下面这个例子
这里myFd打印的是undefined,为什么呢?
其实这就是简单的同步代码先执行,异步代码后执行。
console.log(myFd)是同步代码,所以先执行,但是此时异步回调callback还没有执行,所以myFd没有完成赋值,所以同步打印的是undefined。
那么为什么要将fd拿出来呢?其实就是为了防止回调地狱的产生。
解决方案有两种,Promise和async await,但是都略显复杂。
其实大部分情况下,打开文件都不会花费很长时间,即不会造成严重的同步阻塞,只是基于异步I/O设计,我们习惯使用异步操作去完成打开文件,但是代价却是复杂的代码编写。而Node.js也考虑到了这一点,所以fs模块下的文件操作,基本上都配置同步和异步两种方法。
就比如打开文件操作,有异步方法 fs.open ,也有同步方法 fs.openSync。
同步方法的缺点就是可能因为I/O操作造成同步阻塞,但是如果我们可以预知打开的文件不会造成阻塞的话,比如文件很小,此时我们使用fs.openSync就可以简化代码的书写,方便获取到fd。
此时就能同步地获取到fd了。