Node.js 文件操作详解:路径处理与底层 fs 模块
本文详解Node.js中的文件处理机制,涵盖path模块的路径标准化、连接与解析方法,以及fs模块的底层文件操作。重点讲解文件描述符的使用、open/read/write/close函数的参数与模式标记,以及缓冲区在异步IO中的控制权管理,帮助开发者掌握Node.js文件系统的核心API。
Node.js 文件处理概述
Node.js提供了一组数据流API,允许开发者像处理网络流一样处理文件,使用便捷。然而,这些高层API仅支持顺序处理,无法实现随机读写。因此,进行随机访问或更精细控制时,必须使用更底层的文件系统操作接口。
本章将介绍文件处理的基础知识,包括如何打开文件、读取文件的特定部分、写入数据以及关闭文件等核心操作。
Node.js中的许多文件API与UNIX(POSIX)系统中的对应API高度相似。例如,文件描述符的使用方式与UNIX一致:在Node.js中,文件描述符是一个整型数字,代表进程文件描述符表中某个实体的索引。
存在三个特殊的文件描述符:1、2和3。它们分别代表标准输入、标准输出和标准错误。标准输入是一个只读流,进程通过它从控制台或进程通道读取数据。标准输出和标准错误是仅用于输出的文件描述符,常用于向控制台、其他进程或文件输出数据。其中,标准错误专门负责输出错误信息,而标准输出负责常规进程输出。
进程启动后,即可使用这些文件描述符,尽管它们并不对应实际的物理文件。无法随机读写文件中的特定位置(注:原文可能存在笔误,根据上下文应指不能像普通文件那样随机定位读写),只能像操作网络数据流那样进行顺序读取和输出。一旦数据写入,便无法直接修改已写入的部分。
普通文件不受上述限制。在Node.js中,你可以创建仅支持向尾部追加数据的文件,也可以创建支持随机读写位置的文件。
几乎所有涉及文件的操作都需要处理文件路径。本章首先介绍处理路径的工具函数,随后深入讲解文件的读写与数据操作细节。
处理文件路径
处理文件路径
文件路径分为相对路径和绝对路径两种,用于标识具体文件。你可以合并文件路径、提取文件名信息,甚至检测文件是否存在。
在Node.js中,虽然可以使用字符串直接操作文件路径,但这会使问题复杂化。例如,连接路径的不同部分时,有些部分以“/”结尾而有些没有,且路径分隔符在不同操作系统中可能不同。因此,手动连接路径会导致代码冗长且繁琐。
幸运的是,Node.js提供了path模块,可帮助你标准化、连接、解析路径,将绝对路径转换为相对路径,从路径中提取各部分信息,并检测文件是否存在。总体而言,path模块主要进行字符串处理,且通常不会访问底层文件系统(path.exists函数除外)。
路径的标准化
在存储或使用路径之前,将其标准化通常是一个好习惯。例如,由用户输入、配置文件获得的路径,或由多个路径连接而成的路径,一般都需要标准化。可以使用path模块的normalize函数来标准化路径,该函数还能处理“..”、“.”和“//”等情况。例如:
代码如下:
var path = require('path');
path.normalize('/foo/bar//baz/asdf/quux/..');
// => '/foo/bar/baz/asdf'
连接路径
使用path.join()函数,可以连接任意多个路径字符串。只需将所有路径字符串依次传递给join()函数即可:
代码如下:
var path = require('path');
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// => '/foo/bar/baz/asdf'
如你所见,path.join()内部会自动对路径进行标准化处理。
解析路径
使用path.resolve()可以将多个路径解析为一个绝对路径。其功能类似于对这些路径依次执行“cd”操作。与cd命令的参数不同,这些路径可以是文件,且不必真实存在——path.resolve()方法不会访问底层文件系统来确认路径是否存在,它仅执行字符串操作。
例如:
代码如下:
var path = require('path');
path.resolve('/foo/bar', './baz');
// => /foo/bar/baz
path.resolve('/foo/bar', '/tmp/file/');
// => /tmp/file
如果解析结果不是绝对路径,path.resolve()会将当前工作目录作为前缀附加到解析结果之前。例如:
代码如下:
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果当前工作目录是/home/myself/node, 将返回
// => /home/myself/node/wwwroot/static_files/gif/image.gif'
计算两个绝对路径的相对路径
path.relative()可以告诉你如何从一个绝对地址跳转到另一个绝对地址。例如:
代码如下:
var path = require('path');
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// => ../../impl/bbb
从路径提取数据
以路径“/foo/bar/myfile.txt”为例,如果你想获取父目录(/foo/bar)的内容,或者读取同级目录的其他文件,可以使用path.dirname(filePath)获取文件路径的目录部分。例如:
代码如下:
var path = require('path');
path.dirname('/foo/bar/baz/asdf/quux.txt');
// => /foo/bar/baz/asdf
或者,如果你想从文件路径中提取文件名(即文件路径的最后部分),可以使用path.basename函数:
代码如下:
var path = require('path');
path.basename('/foo/bar/baz/asdf/quux.html')
// => quux.html
文件路径中可能包含文件扩展名,通常是文件名中最后一个“.”字符之后的部分字符串。
path.basename还可以接受一个扩展名字符串作为第二个参数,这样返回的文件名将自动去除扩展名,仅返回文件名称部分:
代码如下:
var path = require('path');
path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// => quux
若要执行此操作,首先需知道文件的扩展名,可以使用path.extname()来获取扩展名:
代码如下:
var path = require('path');
path.extname('/a/b/index.html');
// => '.html'
path.extname('/a/b.c/index');
// => ''
path.extname('/a/b.c/.');
// => ''
path.extname('/a/b.c/d.');
// => '.'
检查路径是否存在
前文涉及的路径处理操作均与底层文件系统无关,仅执行字符串操作。然而,有时你需要判断文件路径是否存在。例如,判断文件或目录是否存在,若不存在则创建它。可以使用path.exists():
代码如下:
var path = require('path');
path.exists('/etc/passwd', function(exists) {
console.log('exists:', exists);
// => true
});
path.exists('/does_not_exist', function(exists) {
console.log('exists:', exists);
// => false
});
注意:从Node 0.8版本开始,exists从path模块移到了fs模块,变成了fs.exists,除了命名空间不同,其他功能不变:
代码如下:
var fs = require('fs');
fs.exists('/does_not_exist', function(exists) {
console.log('exists:', exists);
// => false
});
path.exists()是一个I/O操作,由于它是异步的,因此需要一个回调函数。当I/O操作返回后,调用该回调函数并将结果传递给它。你还可以使用其同步版本path.existsSync(),功能完全相同,只是不调用回调函数,而是直接返回结果:
代码如下:
var path = require('path');
path.existsSync('/etc/passwd');
// => true
fs 模块与底层文件操作
fs模块介绍
fs模块包含所有文件查询和处理的相关函数。使用这些函数,可以查询文件信息、读写和关闭文件。导入fs模块的方式如下:
代码如下:
var fs = require(‘fs')
查询文件信息
有时你需要知道文件的大小、创建日期或权限等文件信息,可以使用fs.stat函数来查询文件或目录的元信息:
代码如下:
var fs = require('fs');
fs.stat('/etc/passwd', function(err, stats) {
if (err) { throw err;}
console.log(stats);
});
这段代码片段会有类似下面的输出
代码如下:
{ dev: 234881026,
ino: 95028917,
mode: 33188,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
size: 5086,
blksize: 4096,
blocks: 0,
atime: Fri, 18 Nov 2011 22:44:47 GMT,
mtime: Thu, 08 Sep 2011 23:50:04 GMT,
ctime: Thu, 08 Sep 2011 23:50:04 GMT }
1.fs.stat()调用会将一个stats类的实例作为参数传递给它的回调函数,可以像下面这样使用stats实例:
2.stats.isFile() —— 如果是个标准文件,而不是目录,socket,符号链接或者设备,则返回true,否则false 3.stats.isDiretory() —— 如果是目录则返回tue,否则false 4.stats.isBlockDevice() —— 如果是块设备则返回true,在大多数UNIX系统中块设备通常都在/dev目录下 5.stats.isChracterDevice() —— 如果是字符设备返回true 6.stats.isSymbolickLink() —— 如果是文件链接返回true 7.stats.isFifo() —— 如果是个FIFO(UNIX命名管道的一个特殊类型)返回true 8.stats.isSocket() —— 如果是个UNIX socket(TODO:googe it)
打开文件
在读取或处理文件之前,必须先使用fs.open函数打开文件。随后,你提供的回调函数会被调用,并得到该文件的描述符,稍后可用此文件描述符来读写已打开的文件:
代码如下:
var fs = require('fs');
fs.open('/path/to/file', 'r', function(err, fd) {
// got fd file descriptor
});
fs.open的第一个参数是文件路径,第二个参数是一些用来指示以什么模式打开文件的标记。这些标记可以是r,r+,w,w+,a或者a+。下面是这些标记的说明(来自UNIX文档的fopen页)
1.r —— 以只读方式打开文件,数据流的初始位置在文件开始 2.r+ —— 以可读写方式打开文件,数据流的初始位置在文件开始 3.w ——如果文件存在,则将文件长度清0,即该文件内容会丢失。如果不存在,则尝试创建它。数据流的初始位置在文件开始 4.w+ —— 以可读写方式打开文件,如果文件不存在,则尝试创建它,如果文件存在,则将文件长度清0,即该文件内容会丢失。数据流的初始位置在文件开始 5.a —— 以只写方式打开文件,如果文件不存在,则尝试创建它,数据流的初始位置在文件末尾,随后的每次写操作都会将数据追加到文件后面。 6.a+ ——以可读写方式打开文件,如果文件不存在,则尝试创建它,数据流的初始位置在文件末尾,随后的每次写操作都会将数据追加到文件后面。
读文件
一旦打开了文件,就可以开始读取文件内容。但在开始之前,你得先创建一个缓冲区(buffer)来存放这些数据。这个缓冲区对象将会以参数形式传递给fs.read函数,并被fs.read填充上数据。
代码如下:
var fs = require('fs');
fs.open('./my_file.txt', 'r', function opened(err, fd) {
if (err) { throw err }
var readBuffer = new Buffer(1024),
bufferOffset = 0,
bufferLength = readBuffer.length,
filePosition = 100;
fs.read(fd,
readBuffer,
bufferOffset,
bufferLength,
filePosition,
function read(err, readBytes) {
if (err) { throw err; }
console.log('just read ' + readBytes + ' bytes');
if (readBytes > 0) {
console.log(readBuffer.slice(0, readBytes));
}
});
});
上面代码尝试打开一个文件,当成功打开后(调用opened函数),开始请求从文件流第100个字节开始读取随后1024个字节的数据(第11行)。
fs.read()的最后一个参数是个回调函数(第16行),当下面三种情况发生时,它会被调用:
1.有错误发生 2.成功读取了数据 3.没有数据可读
如果有错误发生,第一个参数(err)会为回调函数提供一个包含错误信息的对象,否则这个参数为null。如果成功读取了数据,第二个参数(readBytes)会指明被读到缓冲区里数据的大小。如果值是0,则表示到达了文件末尾。
注意:一旦把缓冲区对象传递给fs.open(),缓冲对象的控制权就转移给了read命令。只有当回调函数被调用,缓冲区对象的控制权才会回到你手里。因此在这之前,不要读写或者让其它函数调用使用这个缓冲区对象;否则,你可能会读到不完整的数据,更糟的情况是,你可能会并发地往这个缓冲区对象里写数据。
写文件
通过传递给fs.write()一个包含数据的缓冲对象,来往一个已打开的文件里写数据:
代码如下:
var fs = require('fs');
fs.open('./my_file.txt', 'a', function opened(err, fd) {
if (err) { throw err; }
var writeBuffer = new Buffer('writing this string'),
bufferPosition = 0,
bufferLength = writeBuffer.length, filePosition = null;
fs.write( fd,
writeBuffer,
bufferPosition,
bufferLength,
filePosition,
function wrote(err, written) {
if (err) { throw err; }
console.log('wrote ' + written + ' bytes');
});
});
这个例子里,第2(译者注:原文为3)行代码尝试用追加模式(a)打开一个文件,然后第7行代码(译者注:原文为9)向文件写入数据。缓冲区对象需要附带几个信息一起做为参数:
1.缓冲区的数据 2.待写数据从缓冲区的什么位置开始 3.待写数据的长度 4.数据写到文件的哪个位置 5.当操作结束后被调用的回调函数wrote
这个例子里,filePostion参数为null,也就是说write函数将会把数据写到文件指针当前所在的位置。因为是以追加模式打开的文件,因此文件指针在文件末尾。
跟read操作一样,千万不要在fs.write执行过程中使用哪个传入的缓冲区对象。一旦fs.write开始执行,它就获得了那个缓冲区对象的控制权。你只能等到回调函数被调用后才能再重新使用它。
关闭文件
你可能注意到了,到目前为止,本章的所有例子都没有关闭文件的代码。因为它们只是些仅使用一次而且又小又简单的例子,当Node进程结束时,操作系统会确保关闭所有文件。
但是,在实际的应用程序中,一旦打开一个文件,你要确保最终关闭它。要做到这一点,你需要追踪所有那些已打开的文件描述符,然后在不再使用它们的时候调用fs.close(fd[,callback])来最终关闭它们。如果你不仔细的话,很容易就会遗漏某个文件描述符。下面的例子提供了一个叫openAndWriteToSystemLog的函数,展示了如何小心的关闭文件:
代码如下:
var fs = require('fs');
function openAndWriteToSystemLog(writeBuffer, callback){
fs.open('./my_file', 'a', function opened(err, fd) {
if (err) { return callback(err); }
function notifyError(err) {
fs.close(fd, function() {
callback(err);
});
}
var bufferOffset = 0,
bufferLength = writeBuffer.length,
filePosition = null;
fs.write( fd, writeBuffer, bufferOffset, bufferLength, filePosition,
function wrote(err, written) {
if (err) { return notifyError(err); }
fs.close(fd, function() {
callback(err);
});
}
);
});
}
openAndWriteToSystemLog(
new Buffer('writing this string'),
function done(err) {
if (err) {
console.log("error while opening and writing:", err.message);
return;
}
console.log('All done with no errors');
}
);
在这儿,提供了一个叫openAndWriteToSystemLog的函数,它接受一个包含待写数据的缓冲区对象,以及一个操作完成或者出错后被调用的回调函数。如果有错误发生,回调函数的第一个参数会包含这个错误对象。
注意那个内部函数notifyError,它会关闭文件,并报告发生的错误。
注意:到此为止,你知道了如何使用底层的原子操作来打开,读,写和关闭文件。然而,Node还有一组更高级的构造函数,允许你用更简单的方式来处理文件。
比如,你想用一种安全的方式,让两个或者多个write操作并发的往一个文件里追加数据,这时你可以使用WriteStream。
还有,如果你想读取一个文件的某个区域,可以考虑使用ReadStream。这两种用例会在第九章“数据的读,写流”里介绍。
小结
当你使用文件时,多数情况下都需要处理和提取文件路径信息。通过使用path模块,你可以连接路径、标准化路径、计算路径的差别,以及将相对路径转化成绝对路径。你可以提取指定文件路径的扩展名、文件名、目录等路径组件。
Node在fs模块里提供了一套底层API来访问文件系统,底层API使用文件描述符来操作文件。你可以用fs.open打开文件,用fs.write写文件,用fs.read读文件,并用fs.close关闭文件。
当有错误发生时,你应该总是使用正确的错误处理逻辑来关闭文件——以确保在调用返回前关闭那些已打开的文件描述符。
全栈开发中JavaScript与Java怎么协作?前后端技术栈配合解析
2026-11-04 11:08:24
Node.js官网访问慢?国内镜像加速下载方案
2026-11-03 18:57:24
Node.js 连接 MySQL 数据库实现增删改查操作
2026-11-03 14:31:14
Windows系统安装Node.js:下载、配置与环境验证
2026-11-03 14:15:56
Linux (Ubuntu/CentOS) 环境下安装Node.js的常用方法
2026-11-03 10:18:39
