zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Node.js fs模块(一)文件基本操作

JS文件Node模块 基本操作 fs
2023-09-14 09:13:41 时间

目录

文件基础知识

文件是啥?

文件权限位

文件描述符

文件操作符

fs.open & fs.openSync

fs.open的形参含义

为什么path参数建议使用绝对路径

文件操作符有哪些

文件权限位为啥是三位八进制数

错误优先的回调函数及文件描述符为啥是3开始

如何将fd暴露出去

fs.openSync的使用场景

fs.read & fs.readSync

fs.read(fd, options, callback)

fs.readSync(fd, buffer, options)

关于fs.read中callback的第三个形参buffer指向缓冲区的验证

fs.write & fs.writeSync

fs.write

fs.writeSync

关于文件操作符和fs.write以及fs.writeSync的位置参数position

fs.close


文件基础知识

Node.js fs模块,全称是file system模块,即文件系统模块,顾名思义,这个模块是用来操作文件。而我们常用的文件操作有:打开文件,关闭文件,读取文件数据,写入数据到文件,创建文件夹(目录),删除文件夹(目录),移动文件到文件夹(目录)中。

但是在讨论文件操作之前,我们必须要了解一些文件系统的基本知识。

文件是啥?

其实这个问题不能算是问题,无论是windows系统,还是linux系统,进入之后,我们最常操作的就是文件,比如打开一个txt文本文件,打开一个jpg图片文件,打开一个exe可执行文件,这里其实想说的是文件夹(目录),也算是文件。在文件系统中,文件夹(目录)也算是文件的一种。

文件权限位

使用过linux系统的小伙伴一定对文件权限位很熟悉,而常使用windows系统的小伙伴可能就它不感冒了。其实无论是linux系统还是,windows系统的文件系统都有文件权限位。

文件权限位就是用来说明文件的权限的,即谁可以对文件进行哪些操作。

其中“谁”可以是: 文件所有者,文件所属组,其他用户

“哪些操作”可以是:读,写,执行

每个文件都具备:文件所有者,文件所属组,其他用户 三个用户

每个用户都具备:读,写,执行 三种权限

权限权限位权限值
可读1004
可写0102
可执行0011

所以最大权限值是 7(可读可写可执行),最小权限值是 0(不可读不可写不可执行)

我们可以使用命令窗口输入命令 ls -al,来查看文件的权限

我们选取两个代表,分别是文件夹的权限,文件的权限

所以一个文件完整的权限值 在 000 ~ 777 之间

文件描述符

在文件系统之中,我们可以打开多个文件,那么文件系统如何识别多个文件呢?

通过文件名?那肯定存在文件同名的情况

通过文件内容hash值?那肯定存在拷贝文件,即hash值相同的文件

所以为了能够唯一定位到打开的某个文件,文件系统需要使用文件描述符去描述一个唯一的文件,并且可以根据我呢见描述符找到对应的文件。

windows,linux系统的文件系统都有这套机制,而Node.js 自身的fs模块也有这样一套机制,Node.js 的文件描述符很简单,即第一个打开的文件就标记位0,第二个打开的文件就标记为1,当文件关闭,则回收其文件标识符,方便后续循环利用。

文件操作符

有时候,我们打开一个文件只是为了读取其中的内容,有时候我们打开一个文件是为了修改其中的内容,有时候,我们打开一个文件是为了读取和写入内容,所以如果文件系统知道了我们打开文件的目的是什么,就可以提前准备。所以文件系统在我们打开某个文件前,需要我们给出文件操作符,即我们打开文件的目的是什么。常见的文件操作符有 r (读), w (写),r+(读写),w+(读写)。

那么为什么我们需要告知文件系统我们打开文件的目的呢?

比如,我们读取一个文件的前提是要有这个文件,如果我们读取的文件不存在,则文件系统需要给我们报错。

而我们写入数据到一个文件,则该文件不需要一定是存在的,可以不存在,即当我们写入数据到文件完成后,再创建文件也可以。

所以同样是文件不存在,但是不同的文件操作对应不同的结果。这就是我们需要提供文件操作符给文件系统的原因。

通过上面的基础知识了解,我们发现要操作一个文件,前提必然是打开文件,而打开文件时,需要注意很多东西,比如打开文件会得到一个文件描述符,打开文件前需要指定文件操作符,以及权限位。所以我们来看看fs模块提供的打开文件的操作。

fs.open & fs.openSync

fs.open的形参含义

fs.open(path, flags, mode, callback)参数含义如下

path要打开的文件所在的路径,最好是绝对路径,相对路径会存在问题
flags文件操作符,默认为'r'
mode

文件权限位,默认为 0o666,即所有用户可读可写

callback

异步打开文件后回调函数,回调函数的参数有

err : 打开文件遇到的异常, fd 文件描述符

这里我们对输入的实参值逐一解释下:

为什么path参数建议使用绝对路径

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,而三位二进制数就对应一位八进制数。

错误优先的回调函数及文件描述符为啥是3开始

第四个参数是一个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找到对应的文件。

如何将fd暴露出去

在这里我们其实可以思考一个问题,如何将callback中的fd暴露出去,即暴露到callback外面,例如下面这个例子

这里myFd打印的是undefined,为什么呢?

其实这就是简单的同步代码先执行,异步代码后执行。

console.log(myFd)是同步代码,所以先执行,但是此时异步回调callback还没有执行,所以myFd没有完成赋值,所以同步打印的是undefined。

那么为什么要将fd拿出来呢?其实就是为了防止回调地狱的产生。

解决方案有两种,Promise和async await,但是都略显复杂。

fs.openSync的使用场景

其实大部分情况下,打开文件都不会花费很长时间,即不会造成严重的同步阻塞,只是基于异步I/O设计,我们习惯使用异步操作去完成打开文件,但是代价却是复杂的代码编写。而Node.js也考虑到了这一点,所以fs模块下的文件操作,基本上都配置同步和异步两种方法。

就比如打开文件操作,有异步方法 fs.open ,也有同步方法 fs.openSync。

同步方法的缺点就是可能因为I/O操作造成同步阻塞,但是如果我们可以预知打开的文件不会造成阻塞的话,比如文件很小,此时我们使用fs.openSync就可以简化代码的书写,方便获取到fd。

此时就能同步地获取到fd了。

fs.read & fs.readSync

fs也提供了读取文件数据的方法,包括异步读取fs.read,以及同步读取fs.readSync

fs.read(fd, options, callback)

形参含义解析

fd文件描述符,即从fd指定的文件中读取数据

options

对象

buffer读取的文件数据将写入的缓冲区,默认大小16KB
offset读取的文件数据从缓冲区buffer哪个起始位置开始写入
length读取的文件数据字节数
position从文件的哪个字节位置开始读取

callback

函数

err读取文件过程产生的异常信息
bytesRead读取文件数据的字节数
buffer对应options.buffer指向的缓冲区对象

比较难以理解参数是options.buffer,我们读取文件时,文件一般在磁盘上,而我们的读取动作,是将磁盘上文件的数据,读取到内存中。所以读取文件时,我们需要先准备一个内存空间,用来缓存读取到的文件数据。而Buffer对象刚好支持在申请内存空间,而fs.open的options.buffer形参就是需要我们传入申请的缓冲区。

另外options另外几个位置信息也比较混乱,不容易理解。

options.offset指的是从缓冲区buffer的哪个字节位置开始插入数据,它是和缓冲区相关的。

options.length指的是要读取多少个字节的文件数据,它是和被读取的文件相关的。

options.position指的是从文件的哪个字节位置开始读,它是和被读取的文件相关的。

而callback回调函数依旧是错误优先的,即第一个参数是err,而第二个参数bytesRead指的是读取结束后,一共读取了多少个字节的数据,第三个参数指的是缓冲区 

上面是回调地狱的写法,我们用Promise改进下

 结果发现反而更复杂,逻辑关系更不清晰了,所以我们再使用async await解决下

可以发现逻辑却是清晰了不少,但是相比较于回调地狱写法,还是过于复杂。其实当回调嵌套不深时,完全可以使用回调地狱的写法,大家不要对回调地狱产生抵触心理,在没有Promise之前,大家不都用的是回调嵌套吗解决的异步串行嘛。只有当回调嵌套层级过深,已经影响代码阅读,和质量维护时,才有必要转成async await写法。而有了async await后,一般不再使用Promise去处理异步串行了,更多地是使用Promise处理异步并行,如Promise.all的使用。

fs.readSync(fd, buffer, options)

形参解释:

fd 指定要读取的文件

buffer 是读取的数据将要写入的缓冲区

options.offset 指的是从缓冲区buffer的哪个字节位置开始插入数据,它是和缓冲区相关的。

options.length 指的是要读取多少个字节的文件数据,它是和被读取的文件相关的。

options.position 指的是从文件的哪个字节位置开始读,它是和被读取的文件相关的。

fs.readSync的返回值就是读取数据的字节个数。

关于fs.read中callback的第三个形参buffer指向缓冲区的验证

fs.write & fs.writeSync

fs模块还提供了写入数据到文件的方法,异步写入fs.write和同步写入fs.writeSync

fs.write

形参含义解析

fdfd指定了要被写入数据的文件
buffer写入数据所在的缓冲区
offset缓冲区哪个位置开始输出写入数据
length要写入多少个字节的数据
position从文件的哪个字节位置开始写入
callback
err写入过程发生的异常
bytesWritten写入了多少个字节的数据
buffer对应缓冲区对象

需要注意的是,fs.write操作的fd指向的文件的文件操作符,需要是a 或者 w相关的,否则无法成功写入数据。a操作符标识追加数据到文件中,不会发生覆盖,w操作符标识写入数据到文件中,会发生覆盖。

 即 如果选择 a 追加数据的话,完全可以省略length,position两个位置参数,这两个参数用于控制数据写入到文件的位置。

另外需要注意的是 callback形参中,bytesWritten是实际写入的数据个数,但是buffer却不是实际写入的数据对应的buffer对象,它对应的缓冲区。

fs.writeSync

形参解析

fdfd指定了要被写入数据的文件
buffer写入数据所在缓冲区
offset从缓冲区哪个位置开始捞取写入数据
length写入多少个数据
position从文件哪个字节位置开始写入数据

 返回值解析:fs.writeSync返回写入数据的字节个数

关于文件操作符和fs.write以及fs.writeSync的位置参数position

其实我感觉这里fs.write和fs.writeSync关于位置参数position的设计挺鸡肋的,

因为当文件是a追加状态打开的话,则文件写入位置position不生效

比如这里虽然设置position = 5,但是依旧是在文件内容尾部追加。

还有如果文件操作符设置为 w ,标识覆盖写入,如果position不从0开始的话,会造成position前面的位置被写入0x00数据。

 这肯定不是我们想要的结果。

此时我们需要设置文件操作符为 “r+” 才能实现覆盖文件指定字节范围的数据

fs.close

通过前面的描述,我们已经知道了操作文件前,需要fs.open打开文件获得文件描述符fd,之后fs.read,fs.write需要通过fd获取的指定文件进行读写操作,当读写文件完成后,我们就需要关闭文件,即归还文件描述符fd给文件系统,释放文件资源。

形参解析:

fd:指向要关闭的文件

callback:错误优先的回调函数,且只有一个err形参描述关闭失败的情况