本文详细解析Node.js中fs模块的使用方法,涵盖文件的打开、读取、写入、关闭及删除等底层操作,以及更便捷的文件和目录读写接口。内容包含异步与同步模式的区别、文件权限标志详解、目录遍历算法实现及监听机制,帮助开发者掌握Node.js文件系统的核心功能。
fs模块概述
Node.js通过标准POSIX函数封装提供了文件I/O功能,开发者需使用 require('fs') 引入该模块。所有API均提供异步和同步两种形式。
异步方法的最后一个参数始终为完成回调。回调参数因方法而异,但第一个参数固定用于传递异常。若操作成功,该参数为 null 或 undefined。
//异步示例
var fs = require('fs');
fs.unlink('/tmp/hello', function(err){
if (err) throw err;
console.log('successfully deleted /tmp/hello');
});
同步方法若发生异常会立即抛出,可通过 try/catch 捕获处理,或允许其向上传播。
//同步示例
var fs = require('fs');
fs.unlinkSync('/tmp/hello');
console.log('successfully deleted /tmp/hello');
异步方法不保证执行顺序,直接并行调用可能导致逻辑错误。
fs.rename('/tmp/hello', '/tmp/world', function(err){
if (err) throw err;
console.log('renamed complete');
});
fs.stat('/tmp/world', function(err, stats){
if (err) throw err;
console.log('stats: ${JSON.stringify(stats)}');
});
例如 fs.stat 可能在 fs.rename 之前执行,正确做法是将回调链式嵌套以确保顺序。
fs.rename('/tmp/hello', '/tmp/world', function(err){
if (err) throw err;
fs.stat('/tmp/world', function(err, stats){
if (err) throw err;
console.log('stats: ${JSON.stringify(stats)}');
});
});
官方推荐开发者使用异步版本,因为同步版本会阻塞整个进程直至完成,导致所有连接停滞。
fs底层操作接口
1、打开文件 fs.open(path, flags[, mode], callback)
参数说明如下:
path <String> | <Buffer> 文件路径
flags <String> | <Number> 文件打开标志
mode <Integer> 设置文件权限(含sticky位),仅在文件创建时生效,默认为 0666(可读写)。
callback <Function> 回调函数包含两个参数:(err错误, fd文件描述符)。
flags可选值包括:
'r' - 只读模式。文件不存在则抛出异常。
'r+' - 读写模式。文件不存在则抛出异常。
'rs+' - 同步读写模式。强制操作系统绕过本地文件系统缓存。
'w' - 写入模式。文件不存在则创建,存在则截断。
'wx' - 类似 'w',但若路径已存在则失败。
'w+' - 读写模式。文件不存在则创建,存在则截断。
'wx+' - 类似 'w+',但若路径已存在则失败。
'a' - 追加模式。文件不存在则创建。
'ax' - 类似 'a',但若路径已存在则失败。
'a+' - 读取和追加模式。文件不存在则创建。
'ax+' - 类似 'a+',但若路径已存在则失败。
[注意]使用'rs+'模式不会使fs.open()进入同步阻塞调用。若需同步阻塞,应使用fs.openSync()。
var fs = require('fs');
fs.open('a.txt','r',function(err,fs){
console.log(err);//null
console.log(fs);//3
})
var fs = require('fs');
fs.open('b.txt','r',function(err,fs){
/*
{ Error: ENOENT: no such file or directory, open 'D:\project\b.txt'
at Error (native)
errno: -4058,
code: 'ENOENT',
syscall: 'open',
path: 'D:\\project\\b.txt' }
*/
console.log(err);
console.log(fs);//undefined
})
回调函数中的第二个参数fd代表文件描述符,类似于定时器标识,用于唯一标识打开的文件,且随打开顺序递增。
var fs = require('fs');
fs.open('1.txt','r',function(err,fs){
console.log(fs);//3
})
fs.open('2.txt','r',function(err,fs){
console.log(fs);//4
})
fs.openSync(path, flags[, mode])
fs.open() 的同步版本,返回表示文件描述符的整数。
var fs = require('fs');
var result = fs.openSync('1.txt','r');
console.log(result);//3
2、读取文件 fs.read(fd, buffer, offset, length, position, callback)
参数说明如下:
fd <Integer> 通过 fs.open() 返回的文件描述符
buffer <String> | <Buffer> 数据写入的目标缓冲区
offset <Integer> buffer中开始写入的偏移量
length <Integer> 指定要读取的字节数(整数)
position <Integer> 指定从文件中开始读取的位置(整数)。若position为null,则从当前文件位置开始读取。
callback <Function> 回调包含三个参数:(err, bytesRead, buffer)。err为错误信息,bytesRead表示读取的字节数,buffer为缓冲区对象。
由于read()方法将文件内容读取至buffer对象,因此需提前准备一个Buffer对象。
var fs = require('fs');
fs.open('1.txt','r',function(err,fd){
if(err){
console.log('文件打开失败');
}else{
var bf = Buffer.alloc(5);
fs.read(fd,bf,0,3,null,function(err,len,buffer){
console.log(err);//null
console.log(len);//3
console.log(buffer);//<Buffer 61 61 61 00 00>
})
}
});
fs.readSync(fd, buffer, offset, length, position)
fs.read() 的同步版本,返回 bytesRead 的数量。
var fs = require('fs');
var fd = fs.openSync('1.txt','r');
var bf = Buffer.alloc(5);
var result = fs.readSync(fd,bf,0,3,null);
console.log(result);//3
3、写入文件 fs.write(fd, buffer, offset, length[, position], callback)
参数说明如下:
fd <Integer> 文件描述符
buffer <String> | <Buffer> 要将buffer中的数据写入到文件中
offset <Integer> buffer对象中要写入的数据的起始位置
length <Integer> 指定要写入的字节数
position <Integer> 指定从文件开始写入数据的位置的偏移量。若 typeof position !== 'number',则数据从当前位置写入。
callback <Function> 回调包含三个参数:(err, written, buffer),其中written指定从buffer写入了多少字节。
[注意]多次对同一文件使用fs.write且不等待回调是不安全的。对此情况,强烈推荐使用 fs.createWriteStream。
当我们要对打开的文件进行写操作的时候,打开文件的模式应该是读写模式。
var fs = require('fs');
fs.open('1.txt','r+',function(err,fd){
if(err){
console.log('文件打开失败');
}else{
var bf = Buffer.from('test');
fs.write(fd,bf,0,3,null,function(err,len,buffer){
console.log(err);//null
console.log(len);//3
console.log(buffer);//<Buffer 74 65 73 74>
})
}
});
fs.write(fd, data[, position[, encoding]], callback)
该方法将data写入fd指定的文件。若data不是Buffer实例,则强制转换为字符串。
与写入buffer不同,该方法必须写入整个字符串,不能指定子字符串,因为结果数据的字节偏移量可能与字符串的偏移量不同。
fd <Integer> 文件描述符
data <String> | <Buffer> 要将string或buffer中的数据写入到文件中
position <Integer> 指向从文件开始写入数据的位置的偏移量。若 typeof position !== 'number',则数据从当前位置写入。
encoding <String> 期望的字符串编码
callback <Function> 回调包含三个参数:(err, written, str),其中written指定从str写入了多少字节。
var fs = require('fs');
fs.open('1.txt','r+',function(err,fd){
if(err){
console.log('文件打开失败');
}else{
fs.write(fd,'12345',function(err,len,str){
console.log(err);//null
console.log(len);//5
console.log(str);//<Buffer 74 65 73 74>
})
}
});
fs.writeSync()
fs.write() 的同步版本,返回写入的字节数。
var fs = require('fs');
var fd = fs.openSync('1.txt','r+');
var bf = Buffer.alloc(5);
var result = fs.writeSync(fd,bf,0,3,null);
console.log(result);//3
4、关闭文件 fs.close(fd, callback)
文件操作完成后,应及时关闭文件以释放资源。
参数说明如下:
fd - 通过 fs.open() 方法返回的文件描述符。
callback - 回调函数,无参数。
var fs = require('fs');
fs.open('1.txt','r+',function(err,fd){
if(err){
console.log('文件打开失败');
}else{
fs.close(fd, function(err){
if (err){
console.log(err);
}
console.log("文件关闭成功");
});
}
});
fs.closeSync(fd)
fs.close(fd, callback)的同步版本,返回undefined。
var fs = require('fs');
var fd = fs.openSync('1.txt','r+');
fs.closeSync(fd);
便捷文件操作接口
前述为底层操作,接下来介绍更便捷的文件操作接口。使用下列方法时,无需手动打开和关闭文件,直接操作即可。
1、写入文件
fs.writeFile(file, data[, options], callback)
异步将数据写入文件。若文件不存在则新建,若文件已存在则被替换。
参数说明如下:
file - 文件名或文件描述符。
data - 要写入文件的数据,可以是 String(字符串) 或 Buffer(流) 对象。
options - 该参数是一个对象,包含 {encoding, mode, flag}。默认编码为 utf8, 模式为 0666 , flag 为 'w'。
callback - 回调函数,仅包含错误信息参数(err),在写入失败时返回。
var fs = require('fs');
var filename = '1.txt';
fs.writeFile(filename,'hello',function(err){
console.log(err);//null
})
fs.writeFileSync(file, data[, options])
fs.writeFile() 的同步版本,返回 undefined。
var fs = require('fs');
var filename = '1.txt';
fs.writeFileSync(filename,'abc');
2、追加文件
fs.appendFile(filename, data, [options], callback)
异步追加数据到文件。若文件不存在则创建。data 可以是字符串或 buffer。
参数说明如下:
file - 文件名或文件描述符。
data - 要写入文件的数据,可以是 String(字符串) 或 Buffer(流) 对象。
options - 该参数是一个对象,包含 {encoding, mode, flag}。默认编码为 utf8, 模式为 0666 , flag 为 'w'。
callback - 回调函数,仅包含错误信息参数(err),在写入失败时返回。
var fs = require('fs');
var filename = '1.txt';
fs.appendFile(filename,' world',function(err){
console.log(err);//null
})
fs.appendFileSync(file, data[, options])
fs.appendFile()的同步版本,返回undefined。
var fs = require('fs');
var filename = '1.txt';
fs.appendFileSync(filename,' lalala');
3、读取文件
fs.readFile(file[, options], callback)
参数说明如下:
file - 文件名或文件描述符
options - 该参数是一个对象,包含 {encoding, flag}。默认编码为null,即如果字符编码未指定,则返回原始的 buffer;flag默认为'r'。
callback - 回调函数包含两个参数:(err, data)。data是文件内容(buffer对象),err是错误信息参数,在写入失败时返回。
var fs = require('fs');
var filename = '1.txt';
fs.readFile(filename,function(err,data){
if(err){
console.log('文件读取失败');
}else{
console.log(data);//<Buffer 61 62 63 20 77 6f 72 6c 64 20 6c 61 6c 61 6c 61>
console.log(data.toString());//'abc world lalala'
}
});
fs.readFileSync(file[, options])
fs.readFile的同步版本,返回file的内容。
如果指定了encoding选项,则该函数返回一个字符串,否则返回一个buffer。
var fs = require('fs');
var filename = '1.txt';
var result = fs.readFileSync(filename);
console.log(result);//<Buffer 61 62 63 20 77 6f 72 6c 64 20 6c 61 6c 61 6c 61>
console.log(result.toString());'abc world lalala'
4、删除文件
fs.unlink(path, callback)
参数说明如下:
path - 文件路径。
callback - 回调函数,无参数。
var fs = require('fs');
var filename = '1.txt';
fs.unlink(filename, function(err) {
if (err) {
return console.log('删除失败');
}
console.log("删除成功");
});
fs.unlinkSync(path)
fs.unlink(path, callback)的同步版本,返回值为undefined。
var fs = require('fs');
var filename = '1.txt';
fs.unlink(filename);
5、重命名
fs.rename(oldPath, newPath, callback)
参数说明如下:
oldPath <String> | <Buffer>
newPath <String> | <Buffer>
callback <Function> 回调只有一个可能的异常参数。
var fs = require('fs');
var filename = 'a.txt';
fs.rename(filename,'2.new.txt',function(err){
console.log(err);//null
})
fs.renameSync(oldPath, newPath)
fs.rename(oldPath, newPath, callback)的同步版本,返回undefined。
var fs = require('fs');
var filename = '2.new.txt';
var result = fs.renameSync(filename,'a.txt');
6、文件信息
fs.stat(path, callback)
fs.stat()执行后,会将stats类的实例返回给其回调函数。可通过stats类中的提供方法判断文件的相关属性。
参数说明如下:
path - 文件路径。
callback - 回调函数,带有两个参数如:(err, stats), stats 是 fs.Stats 对象。
var fs = require('fs');
var filename = 'a.txt';
fs.stat(filename,function(err,stats){
console.log(err);//null
/*
{ dev: 223576,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: undefined,
ino: 7599824371527537,
size: 0,
blocks: undefined,
atime: 2017-06-03T14:18:15.370Z,
mtime: 2017-06-03T14:18:15.370Z,
ctime: 2017-06-03T16:32:05.776Z,
birthtime: 2017-06-03T14:18:15.370Z }
*/
console.log(stats);
});
stats类中的方法有:
stats.isFile() 如果是文件返回 true,否则返回 false。
stats.isDirectory() 如果是目录返回 true,否则返回 false。
stats.isBlockDevice() 如果是块设备返回 true,否则返回 false。
stats.isCharacterDevice() 如果是字符设备返回 true,否则返回 false。
stats.isSymbolicLink() 如果是软链接返回 true,否则返回 false。
stats.isFIFO() 如果是FIFO,返回true,否则返回false。FIFO是UNIX中的一种特殊类型的命令管道。
stats.isSocket() 如果是 Socket 返回 true,否则返回 false。
var fs = require('fs');
var filename = 'a.txt';
fs.stat(filename,function(err,stats){
console.log(stats.isFile());//true
});
fs.statSync(path)
fs.stat(path, callback)方法的同步版本,返回一个 fs.Stats 实例。
var fs = require('fs');
var filename = 'a.txt';
var result = fs.statSync(filename);
/*
{ dev: 223576,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: undefined,
ino: 7599824371527537,
size: 0,
blocks: undefined,
atime: 2017-06-03T14:18:15.370Z,
mtime: 2017-06-03T14:18:15.370Z,
ctime: 2017-06-03T16:32:05.776Z,
birthtime: 2017-06-03T14:18:15.370Z }
*/
console.log(result);
7、监听
fs.watch(filename[, options][, listener])
该方法用于监视filename的变化,filename可以是一个文件或一个目录。返回的对象是一个fs.FSWatcher。
参数说明如下:
filename <String> | <Buffer>
options <String> | <Object> 参数可选,如果options是一个字符串,则它指定了encoding。否则options应该以一个对象传入。
persistent <Boolean> 指明如果文件正在被监视,进程是否应该继续运行。默认为true。
recursive <Boolean> 指明是否全部子目录应该被监视,或只是当前目录。适用于当一个目录被指定时,且只在支持的平台。默认为false。
encoding <String> 指定用于传给监听器的文件名的字符编码。默认为'utf8'。
listener <Function> 回调函数包含两个参数:(eventType, filename)。eventType可以是'rename'或'change',filename是触发事件的文件的名称。
回调中提供的 filename 参数仅在 Linux 和 Windows 系统上支持。即使在支持的平台中,filename 也不能保证提供。因此,不要以为 filename 参数总是在回调中提供,如果它是空的,需要有一定的后备逻辑。
fs.watch('somedir', (eventType, filename) => {
console.log(`事件类型是: ${eventType}`);
if (filename) {
console.log(`提供的文件名: ${filename}`);
} else {
console.log('未提供文件名');
}
});
var fs = require('fs');
var filename = '1.txt';
fs.watch(filename,function(eventType, _filename){
console.log(eventType);//change
if(_filename){
console.log(_filename + '发生了改变');//'1.txt发生了改变'
}else{
console.log('...');
}
})
[注意]当一个文件出现或消失在一个目录里时,'rename'也会被触发。
目录操作接口
1、创建
fs.mkdir(path[, mode], callback)
参数说明如下:
path - 文件路径。
mode - 设置目录权限,默认为 0777。
callback - 回调函数,仅包含一个可能的异常参数。
var fs = require('fs');
fs.mkdir('./1',function(err){
console.log(err);//null
})
fs.mkdirSync(path[, mode])
fs.mkdir(path[, mode], callback)的同步版本,返回undefined。
var fs = require('fs');
fs.mkdirSync('./2');
2、删除
fs.rmdir(path, callback)
参数说明如下:
path - 文件路径。
callback - 回调函数,仅包含一个可能的异常参数。
var fs = require('fs');
fs.rmdir('./1',function(err){
console.log(err);//null
})
fs.rmdirSync(path, callback)
fs.rmdir(path, callback)的同步版本,返回undefined。
var fs = require('fs');
fs.rmdirSync('./2');
3、读取
fs.readdir(path[, options], callback)
参数说明如下:
path <String> | <Buffer>
options <String> | <Object> 可选的 options 参数用于传入回调的文件名,它可以是一个字符串并指定一个字符编码,或是一个对象且由一个 encoding 属性指定使用的字符编码。若 encoding 设为 'buffer',则返回的文件名会被作为 Buffer 对象传入。
encoding <String> 默认 = 'utf8'
callback <Function> 回调包含两个参数:(err, files),其中 files 是目录中不包括 '.' 和 '..' 的文件名的数组。
var fs = require('fs');
fs.readdir('./',function(err,data){
console.log(err);//null
/*
[ '.csslintrc',
'.jshintrc',
'a.txt',
'dist',
'Gruntfile.js',
'Gruntfile1.js',
'index.html',
'main.js',
'node_modules',
'package.json',
'src' ]
*/
console.log(data);
})
var fs = require('fs');
fs.readdir('./',function(err,data){
data.forEach(function(item,index,arr){
fs.stat(item,function(err,stats){
if(stats.isFile()){
console.log('文件:' + item);
}
if(stats.isDirectory()){
console.log('目录:' + item);
}
});
})
})
/*
文件:.jshintrc
文件:.csslintrc
目录:dist
文件:Gruntfile.js
文件:index.html
文件:Gruntfile1.js
文件:main.js
目录:node_modules
文件:package.json
文件:a.txt
目录:src
*/
fs.readdirSync(path[, options], callback)
fs.readdir(path[, options], callback)的同步版本,返回一个不包括 '.' 和 '..' 的文件名的数组。
var fs = require('fs');
var result = fs.readdirSync('./');
/*
[ '.csslintrc',
'.jshintrc',
'a.txt',
'dist',
'Gruntfile.js',
'Gruntfile1.js',
'index.html',
'main.js',
'node_modules',
'package.json',
'src' ]
*/
console.log(result);
目录遍历实现
遍历目录是操作文件时的常见需求。例如编写程序查找并处理指定目录下的所有JS文件时,需遍历整个目录。
遍历目录通常使用递归算法,否则难以编写简洁代码。递归算法与数学归纳法类似,通过不断缩小问题规模来解决问题。
function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
上述函数用于计算N的阶乘(N!)。当N大于1时,问题简化为计算N乘以N-1的阶乘。当N等于1时,问题达到最小规模,直接返回1。
目录呈树状结构,遍历时通常使用深度优先+先序遍历算法。深度优先意味着到达节点后首先遍历子节点而非邻居节点。先序遍历意味着首次到达某节点即算遍历完成,而非最后一次返回某节点。因此使用这种遍历方式时,下图的遍历顺序是A > B > D > E > C > F。
A
/ \
B C
/ \ \
D E F
了解必要算法后,可简单实现以下目录遍历函数。
function travel(dir, callback) {
fs.readdirSync(dir).forEach(function (file) {
var pathname = path.join(dir, file);
if (fs.statSync(pathname).isDirectory()) {
travel(pathname, callback);
} else {
callback(pathname);
}
});
}
该函数以某目录为起点。遇到子目录时继续遍历子目录;遇到文件时,将文件绝对路径传给回调函数。回调函数拿到路径后可进行判断和处理。假设存在以下目录结构:
- /home/user/
- foo/
x.js
- bar/
y.js
z.css
使用以下代码遍历该目录时,得到的输入如下。
travel('/home/user', function (pathname) {
console.log(pathname);
});
------------------------
/home/user/foo/x.js
/home/user/bar/y.js
/home/user/z.css
若读取目录或文件状态时使用异步API,目录遍历函数实现会稍复杂,但原理相同。travel函数的异步版本如下。
function travel(dir, callback, finish) {
fs.readdir(dir, function (err, files) {
(function next(i) {
if (i < files.length) {
var pathname = path.join(dir, files[i]);
fs.stat(pathname, function (err, stats) {
if (stats.isDirectory()) {
travel(pathname, callback, function () {
next(i + 1);
});
} else {
callback(pathname, function () {
next(i + 1);
});
}
});
} else {
finish && finish();
}
}(0));
});
更多关于nodeJS中fs模块对文件及目录进行读写,删除,追加,等操作的相关文章大家可以查看下面的相关链接