在Node.js中,与文件系统的交互是非常重要的,特别是如果你需要通过管理动态文件来支持Web应用程序或服务,Node.js在fs模块中提供了与文件系统进行交互的良好接口。该模块提供了在大多数语言中可用的标准文件访问API来打开,读取,写入文件,以及与其交互。
本章说明从Node.js应用程序访问文件系统的基础知识。你应该具备创建,读取和修改文件,以及在目录结构中穿行的能力。你还可用访问文件和文件夹的信息并删除,截断和重命名文件与文件夹。
对于本章所讨论的所有的文件系统调用,你都需要加载fs模块,例如:
var fs = require('fs');
1,同步和异步文件系统调用
Node.js提供的fs模块使得几乎所有的功能都有两种形式可供选择:异步和同步。例如,它提供了异步形式的write()和同步形式的writeSync()。在你实现代码时,理解这两种形式的差异是非常重要的。
同步文件系统调用会阻塞,直到调用完成,控制才被释放回线程。这既具有优势,但如果同步调用阻塞主事件线程或后台线程池中太多的线程,也可能导致在Node.js中严重的性能问题。因此,你应该尽可能地限制使用同步文件系统调用。
异步调用被放置在事件队列中以备随后运行。这使得调用能够融入Node.js事件模型,但在执行你的代码的时候,它可能会有点棘手,因为调用线程将在异步调用被时间循环提取出之前继续运行。
在大多数情况下,同步和异步文件系统调用的底层功能是完全一样的。同步和异步文件系统调用都接受相同的参数,但有个例外:所有的异步调用最终都需要一个额外的参数,即一个在文件系统调用完成时执行的回调函数。
下面介绍了Node.js的同步和异步文件系统调用之间的重要区别:
- 异步调用需要用一个回调函数作为额外的参数。回调函数在文件系统的请求完成时被执行,并且通常包含一个错误作为其第一个参数
- 异步调用自动处理异常;并且如果发送异常,就把一个错误对象作为第一个参数传递。为了在同步调用中处理异常,必须使用try/catch块。
- 同步调用立即运行;并且除非它们完成,否则执行不会返回到当前线程。异步调用被放置在事件队列中,并且执行返回到正在运行的线程的代码,但是实际的调用直到它被事件循环提取出时才会执行。
2,打开和关闭文件
Node.js为打开文件提取同步和异步方式。一旦文件被打开,你就可用从中读取数据或将数据写入它,这取决于用来打开文件的标志。要在Node.js应用程序中打开文件,可使用下面的异步或同步语句之一:)
fs.open(path,flags,[mode],callback)
fs.openSync(path,flags,[mode])
path参数指定文件系统的标准路径字符串。flags参数指定打开文件的模式,如下表所示。可选的mode参数设置文件访问模式,默认为0666,这表示可读且可写。
模式 | 说明 |
r | 打开文件用于读取。如果该文件不存在,则会出现异常 |
r+ | 打开文件用于读写。如果该文件不存在,则会出现异常 |
rs | 在同步模式下打开文件用于读取。这与强制使用fs.openSync()是不一样的。当使用这种模式时,操作系统将绕过本地文件系统缓存。因为它可以让你跳过可能失效的本地缓存,所以这对NFS挂载是有用的。你应该只在必要时使用该标志,因为它可能会对性能产生负面影响 |
rs+ | 同rs,除了打开文件用于读取和写入外 |
w | 打开文件用于写操作 |
wx | 同w,如果路径存在,则打开失败 |
w+ | 打开文件用于读写 |
一旦文件被打开,你需要关闭它以迫使操作系统把更改刷新到磁盘并释放操作系统锁。要关闭文件,可通过使用下列方法之一,并给它传递文件描述符来实现。在异步close()方法调用的情况下,还需要指定一个回调函数:
fs.close(fd,callback)
fs.closeSync(fd)
以下是以异步模式打开和关闭文件的一个例子。请注意,回调函数被指定,并接收err和fd参数。fd参数是你可以用来读取和写入该文件的文件描述符:
fs.open("myFile","w",function(err,fd){
if(!err){
fs.close(fd);
}
});
以下是以同步模式打开和关闭文件的一个例子。请注意,没有回调函数,并且用于读取和写入文件的文件描述符是直接从fs.openSync()返回的:
var fd = fs.openSync("myFile","w");
fs.closeSync(fd);
3,写入文件
fs模块提供了4种不同的方式将数据写入文件。你可以在单个的调用中将数据写入文件,使用同步写操作写入块,采用异步写操作写入块,或通过Writable流来流写入。所有这些方法都接受一个String或Buffer对象作为输入。以下各节介绍这些方法的用法。
(1)简单文件写入
将数据写入一个文件的最简单的方法是使用writeFile()方法中的一种。这些方法把一个字符串或缓冲区的全部内容写入一个文件。以下是writeFile()方法的语法:
fs.writeFile(path,data,[option],callback)
fs.writeFileSync(path,data,[options])
path参数指定文件的路径,这可以是相对或绝对路径。data参数指定将被写入到文件中的String或Buffer对象。可选的options参数是一个对象,它可以包含定义字符串编码,以及打开文件时使用的模式和标志encoding,mode和flag属性。异步方法还需要callback函数,当文件以及完成时它将被调用。
下面清单中的代码显示了如何实现一个简单的异步fileWrite()请求来在文件中存储config(配置)对象的JSON字符串。
var fs = require('fs');
var config = {
maxFiles:20,
maxConnections : 15,
rootPath:"/webroot"
};
var configTxt= JSON.stringify(config);
var options = {encoding:'utf8',flag:'w'};
fs.writeFile('config.txt',configTxt,options,function(err){
if(err){
console.log("Config Write Failed.");
}else{
console.log("Config Saved.");
}
});
(2)同步文件写入
文件写入的同步方法涉及在返回执行正在运行的线程之前,将数据写入文件。这提供了使你能够在相同的代码段写入多次的优点,但如前所述,如果该文件写入控制住其他线程,它就可能是一个缺点。
要同步写入文件,先用openSync()打开它来获取一个文件描述符,然后使用fs.writeSync()将数据写入文件。下面是fs.writeSync()的语法:
fs.writeSync(fd,data,offset,length,position)
下面的代码清单显示了如何实现基本同步写入把一些列字符串数据存储到文件。
var fs = require('fs');
var veggieTray = ['carrots','celery','olives'];
fd = fs.openSync('veggie.txt','w');
while(veggieTray.length){
veggie = veggieTray.pop() + " ";
var bytes = fs.writeSync(fd,veggie,null,null);
console.log("Write %s %d bytes",veggie,bytes);
}
fs.closeSync(fd);
(3)异步写入文件
文件写入的异步方法在事件队列中放置一个写入请求,然后将控制返回给调用代码。除非时间循环提取出写入请求,并且执行它;否则实际的写操作不会发生。在同一个文件上执行多个异步写入请求时,你需要小心。因为除非你在执行下一个写入之前等待第一个写入回调函数完成,否则你不能保证执行的顺序。通常情况下,做到这一点的最简单的方法是把写操作嵌套到来自上以个写操作的回调函数中。下面清单中的代码说明了这一过程。
要异步写入一个文件,首先使用open()打开它,然后在来自请求的回调函数以及执行后,使用fs.write()将数据写入文件。一下是fs.write()的语法。
fs.write(fd,data,offfset,length,position,callback)
其中callback参数必须是可以接收error和bytes两个参数的函数,其中error是在写过程中发生的错误,而bytes指定写入的直接数。
var fs = require('fs');
var fruitBowl = ['xiaoming','xiaohon','xiaobai','xiaohei'];
function writeFruit(fd){
if(fruitBowl.length){
var fruit = fruitBowl.pop() + " ";
fs.write(fd,fruit,null,null,function(err,bytes){
if(err){
console.log("File Write Failed");
}else{
console.log("write %s %d bytes ",fruit,bytes);
writeFruit(fd);
}
});
}else{
fs.close(fd);
}
}
fs.open('fruit.txt','w',function(err,fd){
writeFruit(fd);
});
(4)流式文件写入
往一个文件写入大量数据时,最好的方法之一是使用流,其中包括把文件作为一个Writable流打开。
若要讲数据异步传送到文件,首先需要使用以下语法创建一个Writable对象:
fs.createWriteStream(path,[options])
path参数指定文件的路径,可以是相对路径或绝对路径。可选的options参数是以个对象,它可以包含定义字符串编码以及打开文件时使用的模式和标志的encoding,mode和flag属性。
一旦你打开了Writable文件流,就可以使用标准的流式write(buffer)方法来写入它。当你完成写入后,再调用end()方法来关闭流。
下面清单中的代码显示了如何实现一个基本的Writable文件流,请注意,当代码完成写入后,end()方法在第13行被执行,这回触发close事件。
var fs = require("fs");
var grains = ['wheat','rice','oats'];
var options = {encoding : 'utf8',flag:'w'}
var fileWriteStream = fs.createWriteStream("grains.txt",options);
fileWriteStream.on("close",function(){
console.log("file closed");
});
while(grains.length){
var data = grains.pop() + " ";
fileWriteStream.write(data);
console.log("wrote: %s",data);
}
fileWriteStream.end();
4,读取文件
fs模块提供了4种不同的方法来从文件中读取数据:以一个大块,以写用同步写入的块,以采用异步写入的块,或通过一个Readable流来流式写。所有这些方法都是有效的。具体使用哪一个方法取决于你的应用程序的特定需求。以下分别介绍如何使用和实现这些方法。
(1)简单文件读取
从文件中读取数据最简单的方法是使用readFile()方法中的一种。这些方法从文件中把全部内容读取到数据缓冲区。以下是readFile()方法的语法:
fs.readFile(path,[options],callback)
fs.readFileSync(path,[options])
path参数指定文件的路径,可以是相对路径或绝对路径。可选的options参数是以个对象,它可以包含定义字符串编码以及打开文件时使用的模式和标志的encoding,mode和flag属性。异步方法还需要callback参数,当文件的读取已经完成时,它将被调用。
下面的程序清单读取JSON字符串文件到一个对象
var fs = require('fs');
var options = {encoding:'utf8',flag:'r'};
fs.readFile('config.txt',options,function(err,data){
if(err){
console.log("Failed to open Config File.");
}else{
console.log("Config Loaded.");
var config = JSON.parse(data);
console.log("Max Files: " + config.maxFiles);
console.log("Max Connections: " + config.maxConnections);
console.log("Root Path: " + config.rootPath);
}
});
(2)同步文件读取
文件读取的同步方法是在执行返回到正在运行的线程之前读取文件中的数据。这提供了允许你在代码相同的部分多次读取的优点;但正如前面所讨论的,如果该文件读取操作控制住其他线程,那么它也可能是以个缺点。
要同步读取一个文件,先使用openSync()打开它来获取一个文件描述符,然后使用readSync()从文件中读取数据。下面是readSync()的语法:
fs.readSync(fd,buffer,offset,length,position)
下面是一段代码:
var fs = require('fs');
fd = fs.openSync('veggie.txt','r');
var veggies = "";
do{
var buf = new Buffer(5);
buf.fill();
var bytes = fs.readSync(fd,buf,null,5);
console.log("read %d bytes",bytes);
veggies += buf.toString();
}while(bytes>0);
fs.closeSync(fd);
console.log("Veggies: " + veggies);
(3)异步文件读取
文件读取的异步方法是将读取请求放置在事件队列中,然后将控制返回给调用代码。除非事件循环提取出读请求,并且执行它;否则实际读操作不会发生。在同一文件读取多个异步执行请求时,你必须要小心。因为除非你在执行下一个读取之前等待第一个读取回调执行完成,否则你就不能保证其执行顺序。通常情况下,做到这一点的最简单方法是把读取嵌套在来自上以此读取的回调函数中。
要一步从文件中读取,首先使用open()打开它,然后在来自打开请求的回调函数已经执行后,使用read()读取文件数据。以下是read()的语法:
fs.read(fd,buffer,offset,length,position,callback)
下面是一段执行从文件进行一步读取的代码清单:
var fs = require('fs');
function readFruit(fd,fruits){
var buf = new Buffer(5);
buf.fill();
fs.read(fd,buf,0,5,null,function(err,bytes,data){
if(bytes > 0){
console.log("read %d bytes",bytes);
fruits += data;
readFruit(fd,fruits);
}
else{
console.log("Fruits: %s",fruits);
fs.close(fd);
}
});
}
fs.open('fruit.txt','r',function(err,fd){
readFruit(fd,"");
});
(4)流式文件读取
从文件读取大量数据时使用的最好方法之一是流式读取,这将把一个文件作为一个Readable流打开。
要异步从文件传输数据,你首先需要使用一下语法创建一个Readable流对象:
fs。createReadStream(path,[options])
下面是一段程序清单:
var fs = require('fs');
var options = {encoding:'utf8',flag:'r'};
var fileReadStream = fs.createReadStream("grains.txt",options);
fileReadStream.on('data',function(chunk){
console.log('Grains:%s',chunk);
console.log('Read %d bytes of data.',chunk.length);
});
fileReadStream.on("close",function(){
console.log("File Closed");
});
5,,其他文件系统任务
除了使你可以读取和写入文件,fs模块还提供了与文件系统交互的附加功能。例如,你可以使用它在目录中列出文件,查看文件的信息,等等。以下各节将介绍在创建Node.js应用程序时,你可能需要实现的最常用的文件系统任务。
(1)验证路径的存在性
fs.exists(path,callback) :异步,callback函数可以有一个参数传入 true或false
fs.existsSync(path) : 同步
(2)获取文件信息
另一项常见的任务是获取有关文件系统对象的基本信息,如文件大小,模式。修改时间,以及条目是否是一个或文件夹等。你可以使用下面的调用来获得这样的信息:
fs.stats(path.callback)
fs.statsSync(path)
fsStatsSync()方法返回一个Stats对象。执行fs.stats()方法,而Stats对象作为第二个参数被传递到回调函数。第一个参数是error,如果发生错误的话。
下表列出了一些附加到Stats对象上的最常用的属性和方法。
属性/方法 | 说明 |
isFile() | 如果条目是以个文件,则返回true |
isDirectory() | 如果该条目是一个目录,返回true |
isSocket() | 如果该条目是一个套接字,返回true |
dev | 指定文件所在的设备ID |
mode | 指定文件的访问模式 |
size | 指定文件的字节数 |
blksize | 指定用于存储该文件的块的大小,以字节为单位 |
blocks | 指定文件在磁盘上占用的块的数目 |
atime | 指定上次访问文件的时间 |
mtime | 指定文件的最后修改时间 |
ctime | 指定文件的创建时间 |
(3)列出文件
使用文件系统开展工作时,另一项常见的任务是列出在目录中的文件和文件夹——例如列出一个目录中的文件,以确定是否需要进行清理;在目录结构上动态操作等。
你可以使用下列命令之一读取条目列表来访问文件系统中的文件:
fs.readdir(path,callback):异步,列表作为第二个参数被传递给回调函数,
fs.readdirSync(path):同步,
(4)删除文件
处理文件时,另一项常见任务是删除它们以清理数据或腾出更多文件系统上的空间,要从Node.js中删除文件,请使用下列命令之一:
fs.unlink(path,callback)
fs.unlinkSync(path)
(5)截断文件
截断文件是指通过把文件结束处设置为比当前值小的值来减小文件的大小。你可能需要截断不断增长,但不包含关键数据的文件(例如,临时日志)。可以使用下面的fs调用之一来截断文件,传入你希望文件截断完成时要包含的字节数:
fs.truncate(path,len,callback)
fs.truncateSync(path,len)
(6)建立和删除目录
有时你可能需要实现目录结构来存储你的Node.js应用程序的文件。fs模块提供根据需要添加和删除目录的功能。
从Node.js添加目录,请使用下列fs调用之一:
fs.mkdir(path,[mode],callback)
fs.mkdirSync(path,[mode])
path可以是绝对路径或相对路径。可选的mode参数允许你指定新目录的访问模式。
要从Node.js删除目录,请使用以下的fs调用之一,不管是使用绝对路径还是相对路径:
fs.rmdir(path,callback)
fs.rmdirSync(path)
(7)重命名文件和目录
你可能需要在Node.js应用程序中重命名文件和文件夹,以腾出空间给新的数据,归档旧的数据,或应用由用户所做的更改。重命名文件和文件夹使用以下fs调用完成:
fs.rename(oldPath,newPath,callback)
fs.renameSync(oldPath,newPath)
oldPath参数指定现有的文件或目录的路径,而newPath参数指定新名称。
(8)监视文件更改入
虽然它并不完全稳定,但fs模块提供了监视文件,并在文件发生变化时执行回调函数的有用工具。
为了实现对文件的监视,可使用下面的命令传递你想要监视的文件的path(路径):
fs.watchFile(path,[options],callback)
你也可以传入options,这是以个对象,它包含persistent(持续)和interval属性。如果你想只要文件被监视,就继续运行这个过程,则把persistent属性设为true。interval属性指定你所需的文件更改的轮询时间,以毫秒为单位。
当文件发生变化时,callback函数就会执行,并传递当前和以前的Stats对象。
下面的代码片段每隔5秒监视以个名为log.txt的文件,并使用Stats对象来输出本次和上次文件被修改的时间:
fs.watchFile("log.txt",{persistent:true,interval:5000},function(curr,prev){
console.log("log.txt modified at: " + curr.mtime);
console.log("Previous modification was: " + prev.mtime);
});
6,小结
Node.js提供fs模块,它可以让你与文件系统进行交互。fs模块允许你创建,读取和修改文件。你也可以使用fs模块在目录结构中穿行,查看文件和文件夹的相关信息,以及通过删除和重命名文件与文件夹来更改目录结构。