nodejs的“文件系统”模块可以操作文件。fs(文件系统)模块是nodejs提供的用于访问本地文件系统的功能模块,使用fs模块可以实现文件及目录的创建,写入、删除等操作。
本教程操作环境:windows7系统、nodejs 12.19.0版、Dell G3电脑。
一、fs模块简介
1. 基本概念
fs(file system)
模块是nodejs提供的用于访问本地文件系统的功能模块,它使得运行于nodejs环境下的JavaScript具备直接读写本地文件的能力。
fs模块是nodejs的核心模块之一,只要安装了nodejs,就可以直接使用,不需要单独安装。引入fs模块非常简单:
let fs = require('fs');
接下来就可以调用fs模块的相关接口直接读写文件系统。
fs模块主要提供了以下的接口类:
-
fs.Dir
,目录类。可理解为文件夹。 -
fs.Dirent
,目录项类。通过Dir
类的返回值获得,表示该目录下的一个子项,可能是文件或子目录。 -
fs.FSWatcher
,文件监听类,它可以为一个文件创建一个监听器,当文件变化时触发回调。 -
fs.StatWatcher
,调用fs.watchFile()方法之后的返回值类型,主要用于协助控制事件循环。 -
fs.ReadStream
,读取流。当流式读取文件时需要使用该类。 -
fs.Stats
,文件元信息类。通过该类可以获取文件相关信息(如文件类型、文件大小、文件描述符等)。 -
fs.WriteStream
,写入流。当流式写入数据时需要使用。
除了以上的类,fs模块还提供了非常多的实例方法,它们可以直接通过fs调用,如读取文件的方法reafFile
:
let fs = require('fs'); fs.readFile('./README.md', function(err, data) = { if (err) throw err; ... // 处理数据 })
该函数以异步的方式读取文件,以可能抛出的异常作为回调函数的第一个参数(这样设计的目的是“强制”或者“提醒”开发者去处理可能出现的异常),而真正的文档数据则作为第二个参数。
fs模块中几乎所有默认的读写函数都是异步的,不过它也同时提供了这些函数的同步版本,一般是在函数后面加Sync
。如上面的代码还可以用readFileSync
改写:
let fs = require('fs'); try { let data = fs.readFileSync('./README.md', 'utf8'); } catch(e = { console.error(e); })
大多数情况下,nodejs推荐使用异步版本的读写函数来提升系统性能。而如果要使用同步版本的函数,应该尽可能使用try catch
捕获异常,以防止读写失败造成主线程崩溃。
2. 文件路径
既然是文件系统模块,就必然要根据文件路径找到需要操作的文件。fs模块支持的路径类型分为三类:字符串、Buffer和URL对象。
(1). 字符串
字符串类是最常用的路径类型,包括绝对路径和相对路径。绝对路径指的是相对于文件系统根目录的路径,而相对路径是相对于当前工作目录的路径(即运行node命令时所在的目录,可以通过process.cwd()
获取到)。
当使用绝对路径时,windows和其他操作系统存在一定差异。因为在其他大多数操作系统中,驱动器只有唯一的根目录;而在windows上,则存在多个独立的驱动盘(如C盘、D盘等)。从写法上来看,绝对路径一般是以/
开头,表示驱动盘的根目录:
fs.readFile('/README.md', (err, data) => { ... });
而在windows上,则是以驱动盘开头的:
fs.readFile('d://nodejs/README.md', (err, data) => { ... })
我们知道,一般windows都是使用反斜线作为路径分隔符的。不过在nodejs中作了兼容,使用斜线或者反斜线都可以正确识别,因此下面的写法也是正确的:
fs.readFile('d:\nodejsREADME.md', (err, data) => { ... })
需要注意的是,当使用fs.readdir('c:')
读取C盘目录时,实际上读取的是c:\userxxx
这个用户根目录,必须加双斜线或双反斜线,才可以读取到真正的c盘根目录:fs.readdir('c://')
。
而相对路径则是相对于当前工作目录的路径,通常以../
(回退到上级目录)、./
(当前目录)或当前路径下的某个目录项开头。如:
// 当前目录的上一级目录下的README.md文件 fs.readFileSync('../README.md'); // 当前目录下nodejs文件夹内的README.md fs.readFileSync('./nodejs/README.md'); fs.readFileSync('nodejs/README.md');
./
开头表示当前目录,可以直接省略。
(2). Buffer
Buffer类型的路径和字符串几乎没有差别,只是某些操作系统将文件路径视为二进制字节序列,因此通常需要用Buffer.from('README.md')
的方式将字符串路径转化为二进制字节序列。由于这类操作系统比较少用,这里不再详细介绍。
(3). URL
URL(uniform resource locator,统一资源定位器)是一种通用的资源定位方案,它将任意资源(包括本地文件)都视为网络资源,然后以一种统一的方式定位它们。
fs模块仅支持使用file
协议的URL对象,该协议的前缀为file:///
,路径格式如下:
// 在windows下,定位某个主机上的文件, // 一般用于读写局域网内的共享文件夹 fs.readFileSync(new URL('file:///主机名/README.md')); // 定位本地资源 fs.readFileSync(new URL('file:///c:/README.md'));
以上两种写法都是针对windows平台的,第一种主要用于局域网内的共享文件读取,第二种用于读写本地文件。
对于其他平台,url的格式如下:
fs.readFileSync(new URL('file:///nodejs/README.md'));
它会被转化为绝对路径:/nodejs/README.md
。URL类型的路径不支持相对路径。
二、fs模块的常用类
鉴于fs模块的接口数量较为庞大,这里暂不一一探讨,如果感兴趣,请参考 nodejs中文网 – fs模块。本文主要是介绍以下几个接口类中较为常用的一些api,基本上可以满足大部分文件系统操作的需求:
-
fs.Dir
-
fs.Dirent
-
fs.ReadStream
-
fs.Stats
-
fs.WriteStream
1. fs.Dir(v12.12)
这是fs模块对目录(或称为文件夹)的抽象类。调用fs.opendir()
、fs.opendirSync()
或 fsPromises.opendir()
这三个方法时会返回一个Dir
类型的对象,用于操作打开后的目录。
比如我们要遍历当前文件夹下的所有子目录及文件,可以用下面的代码(v12.12以上版本可用):
async function getChild (path) { // 读取目录子项,返回一个promise let dir = await fs.promises.opendir(path); for await (let dirent of dir) { console.log(dirent.name); }}
关闭目录有三个方法:dir.close()
、dir.close(callback)
和dir.closeSync()
。
dir.close()
返回的是一个promise,向其注册then方法可以在目录关闭后执行回调函数,如dir.close().then((e) => { ... })
;dir.close(callback)
是关闭目录的异步方法,直接传入回调函数,它会在文件关闭后调用;dir.closeSync()
是关闭目录的同步方法,只有目录成功关闭后才会执行后续代码。
dir.path
的值为当前目录的路径,即调用opendir
方法时传入的路径。
读取该目录的目录项的三个方法为:dir.read()
、dir.read(callback)
和dir.readSync()
。
dir.read()
返回的是一个promise数组或null,分别负责读取每一个目录项,主要用于async函数遍历:
async function read (path) { let dir = await fs.promises.opendir(path); for await (let dirent of dir.read()){ ... }}
dir.read(callback)
则是读取目录项的异步版本,每次读取到一个子项,就会调用一次callback;dir.readSync()
是读取目录项的同步版本,返回的是Dirent
类型的数组或null。
2. fs.Dirent(v10.10)
目录项类。当通过fs.opendir()
方法读取一个目录时,它的每一个子项就是一个Dirent
类对象,每个Dirent
对象可能是一个子目录,或者是一个文件。每个Dirent
对象都是文件名和文件类型组成的。
fs.Dirent
提供的方法主要是用于判断目录项类型:
-
dirent.isBlockDevice()
,是否为块设备。 -
dirent.isCharacterDevice()
,是否为字符设备。 -
dirent.isDirectory()
,是否为系统目录。 -
dirent.isFIFO()
,是否为先入先出通道。 -
dirent.isFile()
,是否为普通文件。 -
dirent.isSocket()
,是否为套接字。 -
dirent.isSymbolicLink()
,是否为符号链接,即快捷方式。
另外,dirent.name
的值为目录项的名字。
3. fs.ReadStream
读取流类,用于流式读取文件。该类由fs.createReadStream()
创建并返回。
fs.ReadStream
支持三个事件:
-
close
,读取流关闭事件。 -
open
,读取流打开事件。 -
ready
,读取流就绪事件,在open事件发生后立即触发。
回调函数接收文件描述符fd
作为参数,用于对文件的后续操作。
fs.ReadStream
实例有三个实例属性:
-
readStream.bytesRead
,已读取的字节数。 -
readStream.path
,文件路径。 -
readStream.pending
,文件是否就绪(ready事件发生前,该值为true,发生后变为false)。
4. fs.Stats
提供对文件信息的描述。调用fs.stat()
、fs.lstat()
和fs.fstat()
会返回该类型的对象。
一个fs.Stats
实例包含以下属性:
Stats { dev: 2114, // 设备的数字标识符 ino: 48064969, // 设备的索引号 mode: 33188, // 文件类型和模式 nlink: 1, // 文件的硬链接数 uid: 85, // 该文件拥有者的标识符 gid: 100, // 拥有该文件的群组的标识符 rdev: 0, // 数字型设备表标识符 size: 527, // 文件大小,单位为字节 blksize: 4096, // 文件系统块的大小 blocks: 8, // 文件系统为当前文件分配的块数 atimeMs: 1318289051000.1, // 上次被访问时间 mtimeMs: 1318289051000.1, // 上次被修改的时间 ctimeMs: 1318289051000.1, // 上次更改文件状态的时间 birthtimeMs: 1318289051000.1, // 文件的创建时间 atime: Mon, 10 Oct 2011 23:24:11 GMT, // 以上四个时间的另一种格式 mtime: Mon, 10 Oct 2011 23:24:11 GMT, ctime: Mon, 10 Oct 2011 23:24:11 GMT, birthtime: Mon, 10 Oct 2011 23:24:11 GMT }
每个Stats
对象还可以返回一个bigint
类型的结果,它的每个结果都是bigint
类型,而不是上面的number
类型,这里不再详述。
同时,它还有与Dirent
相同的7个实例方法,来判断当前的设备类型,请参考上述Dirent
。
5. fs.WriteStream
写入流类,用于流式地写入数据。它由fs.createWriteStream()
创建和返回。
与fs.ReadStream
类似,它也有close
、open
和ready
三个事件,两者的用法也是一样的。
每个WriteStream
实例也都有三个实例属性:writeStream.bytesWritten
、writeStream.path
和writeStream.pending
,分别表示当前已写入的字节数、写入流文件路径和是否已就绪,与ReadStream
也是类似的。
三、fs模块的常用方法
fs模块提供了很多读写文件系统资源的函数,上一部分介绍的类主要是对这些函数操作结果的封装。下面来介绍一些常用的方法:
1. fs.access(path [,mode], callback)
检查文件的可用性。
第一个参数path为文件路径,可以是字符串、Buffer或URL类型;
第二个参数为要检查的类型,可能的值包括fs.constants.F_OK
(是否存在,默认值)、fs.constants.R_OK
(是否可读)和fs.constants.W_OK
(是否可写);
第三个参数为回调函数,如果检查失败,则会传入一个Error
对象,否则会传入undefined。
比如我们需要检查当前文件夹下是否存在package.json
,可以这样写:
fs.access('package.json', fs.constants.F_OK, (err) => { if (err) { ... // 文件存在 } else { ... // 文件不存在 }})
一般来说,不应该在检查文件可用性之后立即读写文件,因为从检查文件可用性到实际读写文件的过程中,该文件的状态可能发生变化(比如其他进程操作了该文件),这会导致文件可用性检查失效。因此fs.access
方法通常只用来检查文件的可用性,而要读写文件的话,可以直接调用读写文件的方法,再根据回调函数接收到的可能的异常来判断该文件是否可用。
该方法还有一个同步版本:fs.accessSync(path [,mode])
,前两个参数与异步版本是一致的。当文件检查成功时,该方法返回undefined,否则将抛出异常,用法如下:
try { fs.accessSync('package.json', fs.constants.R_OK | fs.constants.W_OK); console.log('可以读写'); } catch (err) { console.error('无权访问'); }
2. fs.appendFile(path, data[, options], callback)
向文件中追加数据。
path
支持字符串、Buffer和URL类型;data
为要追加的数据,可以是字符串或Buffer实例;options
为配置对象,包含三个参数:
-
encoding,编码方式,默认为
utf8
。 -
mode,模式,默认为
0o666
。该参数为文件的权限描述,0o666
表示为每个用户拥有读写权限。 -
flag,文件操作方式,默认为
a
,即追加。
callback
为函数执行完毕的回调函数,如果追加失败,则第一个参数为错误对象,否则为undefined。例子如下:
fs.appendFile('README.md', '这是要添加的数据', (err) => { if (err) { console.log('数据追加失败'); } else { console.log('数据追加成功'); }})
该方法的同步版本为fs.appendFileSync(path, data[, options])
。与accessSync一样,它也只是移除了最后一个参数callback,通过返回值来判断操作结果,这里不再重复举例。
3. fs.chmod(path, mode, callback)
修改文件的权限。
文件系统中每个文件都有读、写和执行三个权限,用r、w和x表示。其中r的权重为4,w的权重为2,x的权重为1,即当用户拥有读权限时,权限值加4,拥有写权限时权限值加2,拥有执行权限时权限值加1。
权限类型 | 权重 |
---|---|
读权限 | 4 |
写权限 | 2 |
执行权限 | 1 |
这种权重分配使得任何一种权限拥有情况的权限值都是不同的,因此只需要一个八进制数字就可以表示某个用户对某个文件拥有哪些权限。
比如某个用户拥有对某个文件的读写权限,而没有执行权限,则他的权限值为4 + 2 + 0 = 6
。而如果同时拥有这三个权限,则权限值为4 + 2 + 1 = 7
。
对一个文件而言,系统中存在三类用户:文件的拥有者、拥有该文件的用户组和其他用户。文件系统使用三个八进制数字,来表示这三类用户对某个文件的权限。比如当某个文件的权限值为0o761
时,它表示:
-
文件的拥有者具备读、写和执行权限:7 = 4 + 2 + 1
-
文件所属的用户组成员具备读写权限:6 = 4 + 2 + 0
-
其他成员只具备执行权限:1 = 0 + 0 + 1
fs.chmod
方法就是用来修改某个文件权限的。它可以传入三个参数,分别是文件路径、权限值和回调函数。权限值就是上面所讲的三个八进制数字,如0o761
;如果修改失败,则回调函数接收异常对象,否则没有参数。
该方法的同步版本为fs.chmodeSync(path, mode)
,用法与其他同步方法一致。
4. fs.chown(path, uid, gid, callback)
修改文件的拥有者。
第一个参数为文件路径;第二个参数为新的文件拥有者的用户标识符;第三个参数为该文件所属的用户组的标识符,最后一个参数为执行完毕后的回调函数。
uid和gid都是number类型,是文件系统为每个用户和用户组分配的唯一id,callback则会在操作失败时得到一个错误对象。
该方法的同步版本为fs.chownSync(path, uid, gid)
,使用方法与其他同步方法一致。
5. fs.close(fd, callback)
异步地关闭一个文件。
第一个参数fd
是调用fs.open
打开一个文件时,文件系统为该文件分配的一个数字类型的文件描述符;当文件关闭失败时,callback会得到一个异常对象。如:
fs.open('README.md', 'a', (err, fd) => { ... fs.close(fd, (err) => { if (err) {} // 关闭失败 }); })
该方法的异步版本为fs.closeSync(fd)
。
6. fs.constants
fs模块提供的常量,参见fs常量。
7. fs.copyFile(src, dest[, mode], callback)
拷贝一个文件。
第一个参数src
为拷贝源的地址,数据类型与path一致;
第二个参数dest
为目标地址,数据类型也与path一致;
第三个可选参数为拷贝参数,默认情况下,将创建或覆盖(文件已存在时)目标文件。除了这个默认值,还有以下三个可选值:
-
fs.constants.COPYFILE_EXCL
– 如果 dest 已存在,则拷贝操作将失败。 -
fs.constants.COPYFILE_FICLONE
– 拷贝操作将尝试创建写时拷贝(copy-on-write)链接。如果平台不支持写时拷贝,则使用后备的拷贝机制。 -
fs.constants.COPYFILE_FICLONE_FORCE
– 拷贝操作将尝试创建写时拷贝链接。如果平台不支持写时拷贝,则拷贝操作将失败。
第四个参数为为执行之后回调函数,与其他异步函数一致,它也会在失败时接收到一个异常对象。
它的同步版本为fs.copyFileSync(src, dest[, mode])
。
8. fs.createReadStream(path[, options])
创建一个读取流。常见用法为:
let fs = require('fs'); let rs = fs.createReadStream('./1.txt',{ highWaterMark:3, //文件一次读多少字节,默认 64*1024 flags:'r', //默认 'r' autoClose:true, //默认读取完毕后自动关闭 start:0, //读取文件开始位置 end:3, //流是闭合区间 包含start也含end encoding:'utf8' //默认null }); rs.on("open",()=>{ console.log("文件打开") }); // 自动触发data事件 直到读取完毕 rs.on('data',(data)=>{ console.log(data); });
9. fs.createWriteStream(path[, options])
创建一个写入流。常见用法为:
const fs=require('fs'); const path=require('path'); let writeStream=fs.createWriteStream('./test/b.js',{encoding:'utf8'}); //读取文件发生错误事件 writeStream.on('error', (err) => { console.log('发生异常:', err); }); //已打开要写入的文件事件 writeStream.on('open', (fd) => { console.log('文件已打开:', fd); }); //文件写入完成事件 writeStream.on('finish', () => { console.log('写入已完成..'); console.log('读取文件内容:', fs.readFileSync('./test/b.js', 'utf8')); //打印写入的内容 console.log(writeStream); }); //文件关闭事件 writeStream.on('close', () => { console.log('文件已关闭!'); }); writeStream.write('这是我要做的测试内容'); writeStream.end();
10. 基于文件描述符的一组函数
如fs.fchown(fd, callback)
、fs.fchmod(fd, callbakc)
等,它是将第一个参数从path替换为了文件对应的文件描述符,使用这类方法之前需要先通过fs.open`打开文件,获取文件描述符。这里不再详述。
11. fs.mkdir(path[, options], callback)
创建一个目录(即文件夹)。
第一个参数path为要创建文件夹的路径;
第二个参数options支持两个参数:
-
recursive
,是否递归创建父目录,默认为false。 -
mode
,创建的文件夹的权限,默认是0o777。
第三个参数为执行完毕的回调函数,它的第一个参数为可能的异常对象,当recursive
为true
时,它还会得到一个path参数,值为当前操作创建的第一个目录。
而当要创建的目录已经存在时,如果recursive
为false,则会抛出异常,否则不会执行任何操作。
创建目录的用法如下:
fs.mkdir('nodejs/lib', {recursive: true}, (err, path) => { ...})
该代码试图在当前路径下的nodejs文件夹内创建lib文件夹,并且要递归地创建父目录。即假如当前目录下没有nodejs文件夹,则先创建它。创建完成后,如果nodejs文件夹是新创建的,则path就是它的路径;如果nodejs已经存在了,则path是新创建的lib文件夹的路径。
该方法的同步版本为fs.mkdirSync(path[, options])
。
12. fs.open(path[, flags[, mode]], callback)
打开一个文件。
第一个参数为要打开的文件的路径;
第二个参数为打开方式,如r(只读),w(只写),a(追加)等;
第三个参数为文件权限,默认为0o666(读写权限);
最后一个参数为回调函数,它有两个参数,第一个为可能抛出的异常对象,第二个是文件系统为被打开的文件分配的数值类型的文件描述符fd
。
如:
fs.open('README.md', 'r', (err, fd) => { ... fs.close(fd, (err) => {})})
它的同步版本为fs.openSync(path[, flags[, mode]])
。
13. fs.opendir(path[, options], callback)
打开一个目录。
第一个参数为目录的路径;
第二个参数包含两个参数:
-
encoding,编码类型,默认
utf8
; -
bufferSize,操作该目录时要缓冲的目录项的数量,默认值32,值越大则性能越好,但内存消耗会更大
第三个参数为callback,即打开目录后的回调函数。该回调函数可接受两个值,一个是可能的异常对象,另一个是打开的目录对象,类型为fs.Dir
。
它的同步版本为fs.opendirSync(path[, options])
。
14. fs.readFile(path[, options], callback)
异步地读取文件数据。
该方法会默认先用fs.open
打开文件,并在读取完毕后自动关闭文件。因此调用该方法不需要手动打开和关闭文件,不过如果需要频繁地操作文件,则该方法会导致文件被反复打开和关闭,造成性能下降。
path除了可以是文件路径外,还可以是文件描述符。如果传入的是文件描述符,则默认在读取完毕后不会关闭文件,并且后续的读取会接着上次读取的位置继续向后。
options支持以下两个参数:
-
encoding,编码格式,默认为null,实际使用时一般传入
'utf8'
-
flag,读取方式,默认为只读
r
如果只需要指定编码格式,options还可以是一个字符串,如'utf8'
。
回调函数的第一个参数为可能的异常对象,第二个参数则是从文件中读出的数据,可能的数据类型为字符串或Buffer。如
fs.readFile('README.md', 'utf8', (err, data) => { if (err) { console.log('读取失败'); } else { console.log(data); }})
该方法的同步版本为fs.readFileSync(path[, options])
。
另外该方法还有一个可替代方法,fs.read
,请参考fs.read。它是在使用fs.open
打开文件时通用的读取文件的方法,对读取过程的控制粒度更细。在对同一个文件进行频繁的读取操作,一般使用该方法。
15. fs.rename(oldPath, newPath, callback)
将oldPath指定的文件重命名为newPath指定的文件,如果该文件已存在,则覆盖它。
回调函数只接收可能抛出的异常对象。
它的同步版本为fs.renameSync(oldPath, newPath)
。
16. fs.stat(path[, options], callback)
获取某个文件的详细信息。
如
fs.stat('README.md', {bigint: true}, function(err, stats) { console.log(stats.isDirectory()); console.log(stats); });
options仅支持bigint这一个参数,表示返回的stats对象是bigint
类型,而不是通常的number
类型。以上操作的结果请参考fs.Stats
类。
它的同步版本为fs.statSync(path[, options], callback)
。
17. fs.writeFile(file, data[, options], callback)
类似于fs.readFile
,该方法为文件的写方法。
file参数为要写入的文件路径,或文件描述符。
data为要写入的数据,支持<string> 、 <Buffer> 、 <TypedArray> 、<DataView>等。
options支持三个参数:encoding,默认值’utf8’;mode,权限类型,默认值0o666;flag,打开方式,默认值w。
回调函数仅支持一个参数,即可能抛出的异常对象。
该方法的同步版本为fs.writeFileSync(file, data[, options])
。
当使用fs.open
打开一个文件时,一般使用fs.write
进行数据写入,请参考fs.write。该方法可以精确地控制写入位置,并且可以连续写入数据。
总结
以上所列举的只是fs模块中较为常用的一些api,官方文档中还有很多其他用途的接口,感兴趣的请参考nodejs中文网 – fs模块。
如果需要在async函数中使用上述api,可以调用fs模块提供的promise封装版本,如:
let fsPromises = require('fs').promises; // 或let fsPromises = require('fs/promises'); // 或let fs = require('fs'); let fsPromises = fs.promises; fsPromise.readFile().then((err, data) => { ... }) async function print (path) { try { let data = await fsPromise.readFile(path, 'utf8'); console.log(data); // 读取成功,输出data } catch (e => { ... // 读取失败 }) }
【推荐学习:《nodejs 教程》】