nodejs的fs模块API基础应用
- 文件操作的基础知识
- 文件操作常用API
- 大文件操作
- 目录操作的常用API
一、文件操作的基础知识
1.1文件权限位:当前用户/用户组对文件是否具有完全控制权限/未指定权限(默认)(F/0)、写入权限(W/2)、读取权限(R/4)、执行权限(X/1)。
在命令行工具中可以通过cacls/ls -l指令查看文件权限位信息:
cacls test.txt //windows命令
ls -l test.txt //linux命令
关于linux中的ls -l查看文件权限信息的具体分析可以参考这篇博客:https://www.cnblogs.com/nchar/p/3905660.html
在nodejs中的fs模块fs.access(path[,mode],callback)可以查看文件是否具备对应权限:
API官方文档:http://nodejs.cn/api/fs.html#fsaccesspath-mode-callback
mode参数文档:http://nodejs.cn/api/fs.html#file-access-constants
//查看文件是否有写入权限 fs.access('test.txt',fs.constants.W_OK,function(err){ console.log(err ? '当前用户/用户组没有写入权限' : '当前用户/用户组有写入权限'); }); fs.access('test.txt',2,function(err){ console.log(err ? '当前用户/用户组没有写入权限' : '当前用户/用户组有写入权限'); });
同类API还有:fsPromises.access()、 fs.accessSync()。
1.2nodejs中文件的模式设置文件操作权限:
官方文档:http://nodejs.cn/api/fs.html#file-modes
nodejs中fs.chmod() 和 fs.chmodSync()两个API可以直接实现文件的权限设置,也可以在fs.open(path[, flags[, mode]], callback)的mode参数上设置文件的操作权限。
设置方法:个位表示其他人权限、十位表示群组操作权限、百位表示所有者操作权限。如果要设置用户的多个操作权限就将其八进制值相加即可,如设置其他人的操作权限为可写、可执行/搜索的值就是“0o3”;如果要设置所有者操作权限可读可写和其他人操作权限可执行/搜索的值就是“0o601”。
//fs.chmod(path, mode, callback) fs.chmod('test.txt','0o775',(err)=>{ if(err) throw err; console.log("test.txt文件权限修改为:所有者和群组可读、可写、可执行/搜索,其他用户可读、可执行/搜索"); });
设置文件操作权限简单的说就是百位设置控制文件所有者的权限,十位设置控制文件所属用户群组的权限,个位设置控制其他用户的权限。同时设置多个权限就是在不同的位数上将基础的可读4、可写2、可执行/搜索1相加,比如设置可读可写就是(4+2=6)、可读可执行/搜索就是(4+1=5)、可写可执行搜索就是(2+1=3)、可读可写可执行/搜索就是(4+2+1=7);
在所有设置文件控制权限的API中的形参名称都是mode,需要注意的是windows系统下只能更改可写权限(windows默认为0即不设置任何权限限制,可以更该成只写权限即2),并且没有所有者、群组、其他用户之间的权限区别。
1.3nodejs中文件系统标志(flag)表示对文件操作方式:
官方文档:http://nodejs.cn/api/fs.html#file-system-flags
文件系统标志是在文件被打开时,设置当前打开文件的操作权限,确保文件操作的安全,文件系统标志在nodejs中可以通过标志的数值参数来设置,对应的参数可以通过fs.constants查看:
console.log(fs.constants);
全部的文件标志及描述可以查看这里:
1 'a': 打开文件进行追加。 如果文件不存在,则创建该文件。 2 'ax': 类似于 'a' 但如果路径存在则失败。 3 'a+': 打开文件进行读取和追加。 如果文件不存在,则创建该文件。 4 'ax+': 类似于 'a+' 但如果路径存在则失败。 5 'as': 以同步模式打开文件进行追加。 如果文件不存在,则创建该文件。 6 'as+': 以同步模式打开文件进行读取和追加。 如果文件不存在,则创建该文件。 7 'r': 打开文件进行读取。 如果文件不存在,则会发生异常。 8 'r+': 打开文件进行读写。 如果文件不存在,则会发生异常。 9 'rs+': 以同步模式打开文件进行读写。 指示操作系统绕过本地文件系统缓存。 10 --这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。 它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。 11 --这不会将 fs.open() 或 fsPromises.open() 变成同步阻塞调用。 如果需要同步操作,应该使用类似 fs.openSync() 的东西。 12 'w': 打开文件进行写入。 创建(如果它不存在)或截断(如果它存在)该文件。 13 'wx': 类似于 'w' 但如果路径存在则失败。 14 'w+': 打开文件进行读写。 创建(如果它不存在)或截断(如果它存在)该文件。 15 'wx+': 类似于 'w+' 但如果路径存在则失败。
文件系统标志使用的相关注意事项:
--在 Linux 上,以追加模式打开文件时,位置写入不起作用。 内核会忽略位置参数,并始终将数据追加到文件末尾。
--修改文件而不是替换它可能需要将 flag
选项设置为 'r+'
而不是默认的 'w'
。
--某些标志的行为是特定于平台的。 因此,在 macOS 和 Linux 上使用 'a+' 标志打开目录,如下例所示,将返回错误。 而在 Windows 和 FreeBSD 上,将返回文件描述符或 FileHandle。所以建议使用文件系统标志的数值参数。
--在 Windows 上,使用 'w' 标志(通过 fs.open() 或 fs.writeFile() 或 fsPromises.open())打开现有隐藏文件将失败并抛出 EPERM。 可以使用 'r+' 标志打开现有的隐藏文件进行写入。
在fs.open中使用文件系统标志的示例:
1 //通过文件系统标志设置(使用r+而不是使用w是为了解决windows系统下隐藏文件w标志符无效并报错的问题) 2 fs.open('test.txt','r+','0o666',(err,fd)=>{ 3 //打开文件的写操作逻辑 4 }); 5 //通过文件系统标志数值设置 6 fs.open('test.txt',1,'0o666',(err,fd)=>{ 7 //打开文件的写操作逻辑 8 });
1.4文件标识符fd:
fd就是操作系统分配给被打开文件的标识,通过文件标识符就可以追踪到这个文件并对其进行操作,类似于JavaScript定时器的标识符的作用。
文件标识符同样也是数值,系统每标识一个文件就会叠加一次,一般是从3开始,因为在nodejs中0、1、2被标准输入、标准输出、标准错误占用。
官方文档:http://nodejs.cn/api/fs.html#file-descriptors_1
二、文件操作常用API
2.1五个常用的文件模块API:readFile、writeFile、apendFile、copyFile、watchFile,这五个都是异步API,在nodejs中还有对应的同步sync和promiseAPI,这些API只是执行逻辑的差别,API的实现功能和传参基本都是相同的,所以基本就以异步API用作使用解析。
解析完这五个常用API后,基于这五个常用API实现一个基于md时实监听转换成html文件的示例。
2.1.1读取文件fs.readFile:
官方文档:http://nodejs.cn/api/fs.html#fsreadfilepath-options-callback
//fs.readFile(path[, options], callback) fs.readFile(path.resolve('test.txt'),'utf-8',(err, data)=>{ //可以使用相对路径或绝对路径 if(err) throw err; console.log(data); });
2.1.2写入文件fs.writeFile:
官方文档:http://nodejs.cn/api/fs.html#fswritefilefile-data-options-callback
//fs.writeFile(file, data[, options], callback) fs.writeFile(path.resolve('test.txt'),"而或长烟一空,皓月千里", 'utf-8',(err)=>{//这个写入方法会覆盖文件中的原数据 if(err) throw err; });
2.1.3追加写入文件fs.apendFile:
官方文档:http://nodejs.cn/api/fs.html#fsappendfilepath-data-options-callback
//fs.appendFile(path, data[, options], callback) fs.appendFile('./test.txt', ",浮光跃金,静影沉璧,渔歌互答,此乐何极!",'utf-8',(err)=>{ if(err) throw err; }); //文本文件内容:而或长烟一空,皓月千里,浮光跃金,静影沉璧,渔歌互答,此乐何极!
2.1.4拷贝文件fs.copyFile:
官方文档:http://nodejs.cn/api/fs.html#fscopyfilesrc-dest-mode-callback
// fs.copyFile(src, dest[, mode], callback) fs.copyFile('test.txt','aaa.txt',0,(err)=>{ //如果aaax.txt文件不存在,会创建该文件,如果存在会被替换 if(err) throw err; });
2.1.5监控文件fs.watchFile:
官方文档:http://nodejs.cn/api/fs.html#fswatchfilefilename-options-listener
1 // fs.watchFile(filename[, options][, listener]) 2 fs.watchFile('test.txt',{interval:20},(curr, prev)=>{ 3 //interval:20设置多久监控一次文件,但并不是每次监控都会输出信息,而是要在文件发生变化是才会输出监听信息 4 //curr:上一次监听信息,一个fs.stats对象 5 //prev:当前监听信息,一个fs.stats对象 6 //监听信息详细参考官方文旦http://nodejs.cn/api/fs.html#class-fsstats 7 //比如可以通过监听信息stats.mtime(文件修改时间来判断文件是否发生修改) 8 if(curr.mtime !== prev.mtime){ 9 console.log("文件已被修改"); 10 fs.unwatchFile('test.txt');//取消监控 11 } 12 });
2.1.6基于md监听时实转换html文件示例:
实现这个demo需要两个工具模块:基于md文件生成html结构的marked工具模块、监听文件变化刷新浏览器的browser-sync工具模块。
marked官方文档:https://marked.js.org/
npm install marked
npm install browser-sync
1 ### 标题一 2 * 列表项1
1 .markdown-body { 2 box-sizing: border-box; 3 min-width: 200px; 4 max-width: 1000px; 5 margin: 0 auto; 6 padding: 45px; 7 } 8 @media (max-width: 750px) { 9 .markdown-body { 10 padding: 15px; 11 } 12 }
1 const fs = require('fs'); 2 const path = require('path'); 3 const marked = require('marked'); 4 const browserSync = require('browser-sync'); 5 6 let mdPath = path.join(__dirname, process.argv[2]); 7 let cssPath = path.resolve('mdTest.css'); 8 let htmlPath = mdPath.replace(path.extname(mdPath),'.html'); 9 10 11 fs.watchFile(mdPath,(curr, prev)=>{ //监听md的变化 12 if(curr.mtime !== prev.mtime){ //当md发生变化时,生成一次html文件 13 fs.readFile(mdPath, 'utf-8', (err,data)=>{ //读取md文件内容 14 let htmlStr = marked.marked(data); //通过marked将md文件内容转换成HTML结构字符串 15 fs.readFile(cssPath,'utf-8',(err,data)=>{ //读取样式文件内存 16 let retHtml = temp.replace('{{content}}',htmlStr).replace('{{style}}',data); //将md的html结构字符串替换模板中的content标识;将css样式文件内容替换模板中的style标识 17 fs.writeFile(htmlPath, retHtml,(err)=>{ //将转添加好数据的HTML文件字符串写入到html文件中 18 console.log('html 生成成功!'); 19 }); 20 }); 21 }); 22 } 23 }); 24 25 browserSync.init({ //通过监听html文件的变化刷新浏览器页面 26 browser:'', 27 server:__dirname, 28 watch:true, 29 index:path.basename(htmlPath) 30 }); 31 32 const temp = ` 33 <!DOCTYPE html> 34 <html lang="en"> 35 <head> 36 <meta charset="UTF-8"> 37 <title></title> 38 <style> 39 {{style}} 40 </style> 41 </head> 42 <body> 43 <div class="markdown-body"> 44 {{content}} 45 </div> 46 </body> 47 </html> 48 `;
测试:
node .\index.js index.md
当前设备的默认浏览器会直接打开生成的html文件,然后你可以尝试修改md文件,测试浏览器的时实刷新效果:
### 标题一 * 列表项1 * 列表项2
三、大文件操作
在处理大文件时必须要考虑的问题是内存是否能缓存空间是否足够,这是前面介绍的readFile和writeFile两个API的不能用于处理大文件的根本原因,在nodejs的文件系统中提供了open、read、write三个API可以将文件分成多段处理,虽然这不是nodejs文件系统中处理大文件的首选模式,但在这篇博客中未然基础API展开,后面基于流和管道的方式处理大文件也是在这三个API的基础上实现。所以,了解和学习这三个API是处理大文件操作的核心内容。
3.1fs.open(path[, flags[, mode]], callback):
path可以文件的路径字符串、Buffer、网址(URL),flags和mode在第一节中有非常详细的说明分别指定当前打开文件要进行的操作和设置文件的操作权限。
fs.open('test.txt','r',(err,rfd)=>{ if(err) throw err; console.log(rfd); //打印文件标识 //... 需要执行的具体操作逻辑 fs.close(rfd); //关闭文件操作,如果操作结束不关闭会导致系统打开当前文件多次,浪费系统资源 });
3.2fs.read(fd, buffer, offset, length, position, callback):
这个API实现的就是将文件中的数据读取到一个Buffer中。
offset:用来设置向Buffer写入数据的开始位置,可以理解为数组的索引值,从0开始;
length:用来设置当前读取文件中多少个字节的数据,注意长度基于的写入位置不能超出Buffer的内存空间,否则报错;
position:用来设置当前读取操作从文件的那个位置开始读取,可以理解为数组的索引值,从0开始。
callback中的bytesRead、data分别表示当前读取的字节数、data引用的是buffer的引用值。
1 let buf = Buffer.alloc(10); 2 fs.open('test.txt','r',(err,rfd)=>{ 3 if(err) throw err; 4 fs.read(rfd,buf,0,3,0,(err,bytesRead,data)=>{ 5 if(err) throw err; 6 console.log(bytesRead); 7 console.log(data); 8 console.log(buf); 9 fs.close(rfd); 10 }); 11 });
这里测试的test.txt文件内容设置为 1234567890 ,然后测试下面这段代码,将test.txt的数据全部读取到buf中:
1 let buf = Buffer.alloc(10); 2 fs.open('test.txt','r',(err,rfd)=>{ 3 if(err) throw err; 4 fs.read(rfd,buf,0,3,0,(err,bytesRead,data)=>{ 5 if(err) throw err; 6 fs.close(rfd); 7 }); 8 }); 9 fs.open('test.txt','r',(err,rfd)=>{ 10 if(err) throw err; 11 fs.read(rfd,buf,3,3,3,(err,bytesRead,data)=>{ 12 if(err) throw err; 13 fs.close(rfd); 14 }); 15 }); 16 fs.open('test.txt','r',(err,rfd)=>{ 17 if(err) throw err; 18 fs.read(rfd,buf,6,3,6,(err,bytesRead,data)=>{ 19 if(err) throw err; 20 fs.close(rfd); 21 }); 22 }); 23 fs.open('test.txt','r',(err,rfd)=>{ 24 if(err) throw err; 25 fs.read(rfd,buf,9,1,9,(err,bytesRead,data)=>{ //注意这里的读取长度不能再是3,因为Buffer的空间已经只有一个字节,超过一个字节会报错 26 if(err) throw err; 27 console.log(bytesRead); 28 console.log(data); 29 console.log(buf); 30 console.log(buf.toString()); 31 fs.close(rfd); 32 }); 33 });
3.3fs.write(fd, buffer[, offset[, length[, position]]], callback):
这个API实现的是将Buffer中的数据写入到文件中。
参数设置与read一样,offset设置从buffer中的那个位置开始读数据,length读多少个字节的数据,position从文件的那个位置开始写入。
1 let buf = Buffer.from('1234567890'); 2 fs.open('test.txt','w',(err,wfd)=>{ 3 if(err) throw err; 4 fs.write(wfd,buf,0,3,0,(err,bytesRead,data)=>{ 5 if(err) throw err; 6 console.log(bytesRead); 7 console.log(buf.toString()); 8 fs.close(wfd); 9 }); 10 });
依然根read一样测试将所有数据写入文件中,注意每次测试的时候记得将文件中的内容清空:
1 let buf = Buffer.from('1234567890'); 2 fs.open('test.txt','w',(err,wfd)=>{ 3 if(err) throw err; 4 fs.write(wfd,buf,0,3,0,(err,bytesRead,data)=>{ 5 if(err) throw err; 6 fs.close(wfd); 7 }); 8 }); 9 fs.open('test.txt','w',(err,wfd)=>{ 10 if(err) throw err; 11 fs.write(wfd,buf,3,3,3,(err,bytesRead,data)=>{ 12 if(err) throw err; 13 fs.close(wfd); 14 }); 15 }); 16 fs.open('test.txt','w',(err,wfd)=>{ 17 if(err) throw err; 18 fs.write(wfd,buf,6,3,6,(err,bytesRead,data)=>{ 19 if(err) throw err; 20 fs.close(wfd); 21 }); 22 }); 23 fs.open('test.txt','w',(err,wfd)=>{ 24 if(err) throw err; 25 fs.write(wfd,buf,9,1,9,(err,bytesRead,data)=>{ 26 if(err) throw err; 27 fs.close(wfd); 28 }); 29 });
3.4基于open、read、write、close模拟实现文件拷贝:
1 //模拟实现文件拷贝 2 let buf = Buffer.alloc(10); 3 const BUFFER_SIZE = buf.length; 4 let readOffset = 0; 5 fs.open('test.txt','r',(err,rfd)=>{ 6 if(err) throw err; 7 fs.open('岳阳楼记.txt','w',(err,wfd)=>{ 8 if(err) throw err; 9 function next(){ 10 fs.read(rfd,buf,0,10,readOffset,(err,bytesRead)=>{ 11 if(err) throw err; 12 if(!bytesRead){ 13 //当bytesRead为0时表示文件数据已经读取完了 14 fs.close(wfd,()=>{}); 15 fs.close(rfd,()=>{}); 16 console.log("拷贝完成!"); 17 return; 18 } 19 readOffset += bytesRead; 20 fs.write(wfd,buf,0,bytesRead,(err,written)=>{ 21 if(err) throw err; 22 next(); 23 }); 24 }); 25 } 26 next(); 27 }); 28 });
最后这里需要补充一下关于read和write的position参数,当不传参或null、-1时,它在一次打开操作中每一次读写都会自动更新到上一次读写的末尾处,也就是说在一次open中不设置position的值连续的读写就类似opendFile的追加写入操作。
四、FS文件系统的目录操作API
access:判断文件或目录是否具有操作权限
stat:获取目录及文件信息
mkdir:创建目录
rmdir:删除目录
readdir:读取目录中内容
unlink:删除指定文件
在第一节中对access判断文件或目录的操作权限已经有详细的介绍,这里就不重复了。
4.1fs.stat(path[, options], callback):获取文件描述信息
第二个可选参数是bigint,即stats中的属性值是否为长整数,默认情况下为false,一般不需要传参,关于bigint的详细内容可以参考这个文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt
fs.stat('test.txt',(err,stats)=>{ if(err) throw err; console.log(stats); });
关于文件描述信息,详细可以参考官方的fs.Stats类:http://nodejs.cn/api/fs.html#class-fsstats
常用的文件描述信息:
stats.size //文件大小(单位字节) stats.atime //最后一次访问次文件的时间戳 stats.mtime //最后一次修改次文件的时间戳 stats.ctime //最后一次更改文件信息状态的时间戳 stats.birthtime //此文件创建的时间戳 stats.isDirectory() //是否是目录 stats.isFile() //是否是文件 stats.isFIFO() //是否是fifo管道 stats.isSocket() //是否是套字节
4.2fs.mkdir(path[, options], callback):创建目录或文件
options可以包含两个属性:recursive(是否递归创建不存在的目录,默认false不递归创建)、mode(设置目录权限)
//创建目录 fs.mkdir('aaa//bbb//ccc.txt',{recursive:true},(err)=>{ if(err) throw err; console.log("创建成功"); });
4.3fs.rmdir(path[, options], callback):删除目录或文件
options包含三个属性:recursive(是否递归删除整个目录,默认false,新版本已弃用,在新版本中使用改属性会报警告并且不执行递归删除操作)、后面两个参数也都是基于recursive实现其功能。
maxRetries //如果递归删除失败,最多重试多少次 retryDelay //如果递归删除失败,间隔多久后重试
递归删除整个目录在新版本中已被弃用,如果需要删除整个目录可以考虑手动实现递归操作。
fs.rmdir('aaa/bbb/ccc.txt',(err)=>{ if(err) throw err; console.log("删除成功"); //这里只删除ccc.txt文件 });
4.4fs.readdir(path[, options], callback):读取目录内容(注意path只能是文件夹,不能是文件,否则会报错)
options包含两个属性:
encoding //读取到的信息编码方式,默认utf-8 withFileTypes//读取信息是否包含Dirent对象,默认为false,回调函的files返回的数组元素是文件或文件夹名称字符串 --如果将withFileTypes设置为true,files则会返回一个对象,包含name属性文件名称字符串,[Symbol(type)]属性描述目录类型(1表示文件、2表示文件夹)
//在测试目录bbb下放一下文件和文件夹 fs.readdir("aaa/bbb",{withFileTypes:false},(err,files)=>{ if(err) throw err; console.log(files); }) fs.readdir("aaa/bbb",{withFileTypes:true},(err,files)=>{ if(err) throw err; console.log(files); })
4.5fs.unlink(path, callback):删除文件/符号链接,在官方文档中明确描述了不适应文件删除,但它确实可以删除文件(不能删除文件夹)。这个方法是系统删除链接的接口,慎用。
fs.unlink('aaa/bbb/ccc.txt',(err)=>{ if(err) throw err; console.log("删除成功"); })
最后添加一些nodejs关于FS文件系统API的简单说明,在nodejs中文件操作有四大类API:异步、同步、异步控制Promise、读写流控制(异步)。这篇博客解析的都是异步API,本质上它们的实现最终功能都一致,只是执行逻辑上的差别,同步异步就不多解释了,关于异步控制Promise在后面的会有详细的异步I/O和异步编程相关博客解析。最后就是读写流相关API:fs.createReadStream(path[, options])、fs.createWriteStream(path[, options]),这部分在后面也会有关于流的详细博客内容来介绍。