本文深入解析Node.js文件系统模块(FS)的异步与同步API,涵盖文件读写、删除及重命名操作。重点探讨Stream流在处理大文件时的优势,通过对比readFile与createReadStream,展示如何利用管道机制优化高并发场景下的内存占用,并提供基于Formidable库的文件上传完整实现示例。
Node.js文件系统模块(FS)基础与异步处理
一、开篇分析
文件系统模块是对标准 POSIX 文件 I/O 操作方法的简单封装。通过调用 require("fs") 即可获取该模块。该模块提供的所有方法均包含异步和同步两种版本。
(1)文件系统模块中的异步方法,其最后一个参数必须是一个完成时的回调函数。
(2)回调函数的具体构成取决于所调用的异步方法。通常,回调函数的第一个参数用于返回错误信息。
(3)若异步操作成功执行并返回,该错误参数将为 null 或 undefined。若使用同步版本的操作方法,一旦出错,将以抛出异常的形式返回错误。
(4)可以使用 try 和 catch 语句来捕获错误,从而确保程序能够继续执行。
首先来看一个读取文件("bb.txt")的简单示例:
(1)创建文件 "bb.txt",内容如下(”大家好,我是大雄君!(*^__^*) 嘻嘻……“)。
(2)读取文件的操作代码如下:
代码如下:
var fs = require("fs") ;
fs.readFile("bb.txt","utf8",function (error,data){
if(error) throw error ;
console.log(data) ;
}) ;
运行结果:

需要注意的是:读取文件时必须指定编码格式,否则默认将以 "buffer" 形式返回。
以下展示未指定编码时的运行效果,区别十分明显。如下:

接下来演示一个写入操作,代码如下:
代码如下:
var fs = require("fs") ;
var txt = "大家要好好学习NodeJS啊!!!" ;
//写入文件
fs.writeFile("bb.txt",txt,function (err) {
if (err) throw err ;
console.log("File Saved !"); //文件被保存
}) ;
运行结果:

再列举一些常用的实例:
代码如下:
// 删除文件
fs.unlink('bb.txt', function(){
console.log('success') ;
}) ;
// 修改文件名称
fs.rename('bb.txt','bigbear.txt',function(err){
console.log('rename success') ;
});
// 查看文件状态
fs.stat('bb.txt', function(err, stat){
console.log(stat);
});
// 判断文件是否存在
fs.exists('bb.txt', function( exists ){
console.log( exists ) ;
}) ;
Fs与Stream之间的联系及大文件处理优化
二、Fs与Stream之间的联系
"Stream" 具有异步特性。我们可以将文件或内容分割为未知数量的指定大小 "chunk" 进行读取,每读取一个 "chunk" 就将其输出,直到文件读取完毕。这类似于 "http1.1" 支持的 "Transfer-Encoding: chunked" 机制。("chunk" 可以以任意形式存在,NodeJS 默认以 "Buffer" 形式存在,这样更高效)。NodeJS 中的 "Stream" 具备 Unix 系统的一个超级特性,即 ("pipe" ------ 管道)。
还记得 “Http模块那篇” 中的第一个 NodeJS 程序 "Hello,大熊!" 吗?我们基于该程序进行修改,代码如下:
(1)建立“bb.html”
代码如下:
<html>
<head>
<style type="text/css">
div {
margin-top: 50px;
width: 100%;
margin: 0px;
height:120px;
line-height:120px;
color:#fff;
font-size:22px;
background:#ff9900;
text-align: center;
}
</style>
</head>
<body>
<div>Hello,大熊!</div>
</body>
</html>
(2)修改之前的程序如下:
代码如下:
var http = require('http') ;
var fs = require("fs") ;
var server = http.createServer(function(req,res){
fs.readFile("bb.html","utf-8", function(err, data){
if(err) {
res.writeHead(500, {'Context-Type': 'text/plain'});
res.end('specify file not exists! or server error!');
}
else{
res.writeHead(200, {'Context-Type': 'text/html'});
res.write(data);
res.end();
}
})
}) ;
server.listen(8888) ;
console.log("http server running on port 8888 ...") ;
以下是运行结果:

现在我们需要思考一个问题:如果我们要发送的不是单纯的文本文件,而是超媒体文件,例如 Google 2014 IO 大会的全程高清视频文件(mp4 格式,时长 2 个多小时,1080p)。
文件大小约为 4 个多 GB。已知 "readFile" 的工作方式是将整个文件读取到内存中,显然如此大的文件不能采用这种方式处理。那该如何解决呢?这时就需要使用 stream 来处理。具体做法如下。
代码如下像这样:
代码如下:
fs.createReadStream(__dirname + '/vedio.mp4').pipe(res) ;
总结一下:
上述代码可以实现所需功能,但在发送文件数据前,服务需要将整个文件数据缓存到内存中。如果 "bb.html" 文件很大且并发量很高,将会浪费大量内存。因为用户必须等待整个文件缓存到内存后才能接收数据,这会导致极差的用户体验。不过幸运的是,"(req, res)" 两个参数都是 Stream,因此我们可以使用 fs.createReadStream() 代替 "fs.readFile()"。
基于Stream的文件上传完整实现示例
三、实例
这里有一个文件上传的小例子:
(1)建立“server.js”
代码如下:
var http = require('http');
var url = require('url');
function start(route, handler) {
function onRequest (request, response) {
var pathname = url.parse(request.url).pathname;
// 路由到相应的业务逻辑
route (pathname, handler, response, request);
}
http.createServer(onRequest).listen(3000);
console.log('server is starting');
}
exports.start = start;
(2)建立“route.js”
代码如下:
function route (pathname, handler, response, request) {
console.log('about to route a request for ' + pathname);
if (typeof handler[pathname] === 'function') {
return handler[pathname](response, request);
} else {
console.log('no request handler found for ' + pathname);
response.writeHead(404, {'Content-Type': 'text/html'});
response.write('404 Not Found!');
response.end();
}
}
exports.route = route;
(3)建立“requestHandler.js”
代码如下:
var querystring = require('querystring'),
fs = require('fs'),
formidable = require('formidable');
function start (response, request) {
console.log('start module');
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" method="post">'+
'<input type="file" name="upload" multiple="multiple">'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {'Content-Type': 'text/html'});
response.write(body);
response.end();
}
function upload (response, request) {
console.log('upload module');
var form = new formidable.IncomingForm();
form.parse(request, function (error, fields, files) {
fs.renameSync(files.upload.path, '/tmp/test.png');
response.writeHead(200, {'Content-Type': 'text/html'});
response.write('You\'ve sent: <br />');
response.write('<img src="/show" />');
response.end();
});
}
function show (response, request) {
console.log('show module');
fs.readFile ('/tmp/test.png', 'binary', function (error, file) {
if (error) {
response.writeHead(200, {'Content-Type': 'text/html'});
response.write(error);
response.end();
} else {
response.writeHead(200, {'Content-Type': 'image/png'});
response.write(file, 'binary');
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show= show;
(4)建立“index.js”
代码如下:
var server = require('./server');
var router = require('./router');
var requestHandler = require('./requestHandler');
var formidable = require('formidable'); // require路径搜索算法??
var handler = {};
handler['/'] = requestHandler.start;
handler['/start'] = requestHandler.start;
handler['/upload'] = requestHandler.upload;
handler['/show'] = requestHandler.show;
server.start(router.route, handler);
总结与最佳实践
四、总结一下
(1)理解 "Fs与Stream之间的联系" 。 (2)熟练使用 "FS" 相关的api。 (3)注意细节的把控,比如: 文件操作api同步方式与异步方式之间的处理细节。 (4)最后强调:理解文件上传例子中的代码组织方式,不断重构,不断总结。