Node.js文件操作详解


Posted in Javascript onAugust 16, 2014

Node有一组数据流API,可以像处理网络流那样处理文件,用起来很方便,但是它只允许顺序处理文件,不能随机读写文件。因此,需要使用一些更底层的文件系统操作。

本章覆盖了文件处理的基础知识,包括如何打开文件,读取文件某一部分,写数据,以及关闭文件。

Node的很多文件API几乎是UNIX(POSIX)中对应文件API 的翻版,比如使用文件描述符的方式,就像UNIX里一样,文件描述符在Node里也是一个整型数字,代表一个实体在进程文件描述符表里的索引。

有3个特殊的文件描述符——1,2和3。他们分别代表标准输入,标准输出和标准错误文件描述符。标准输入,顾名思义,是个只读流,进程用它来从控制台或者进程通道读取数据。标准输出和标准错误是仅用来输出数据的文件描述符,他们经常被用来向控制台,其它进程或文件输出数据。标准错误负责错误信息输出,而标准输出负责普通的进程输出。

一旦进程启动完毕,就能使用这几个文件描述符了,它们其实并不存在对应的物理文件。你不能读写某个随机位置的数据,(译者注:原文是You can write to and read from specific positions within the file.根据上下文,作者可能少写了个“not”),只能像操作网络数据流那样顺序的读取和输出,已写入的数据就不能再修改了。

普通文件不受这种限制,比如Node里,你即可以创建只能向尾部追加数据的文件,还可以创建读写随机位置的文件。

几乎所有跟文件相关的操作都会涉及到处理文件路径,本章先会将介绍这些工具函数,然后再深入讲解文件读写和数据操作

处理文件路径

文件路径分为相对路径和绝对路径两种,用它们来表示具体的文件。你可以合并文件路径,可以提取文件名信息,甚至可以检测文件是否存在。

Node里,可以用字符串来操处理文件路径,但是那样会使问题变复杂,比如你要连接路径的不同部分,有些部分以 “/”结尾有些却没有,而且路径分割符在不同操作系统里也可能会不一样,所以,当你连接它们时,代码就会非常罗嗦和麻烦。

幸运的是,Node有个叫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.exsits():

                   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
                   });

注意:从Node0.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模块:

         var fs = require(‘fs')

查询文件信息

有时你可能需要知道文件的大小,创建日期或者权限等文件信息,可以使用fs.stath函数来查询文件或目录的元信息:

                   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 相关文章推荐
javascript 控制弹出窗口
Apr 10 Javascript
jQuery 页面 Mask实现代码
Jan 09 Javascript
5分钟理解JavaScript中this用法分享
Nov 09 Javascript
JavaScript图片轮播代码分享
Jul 31 Javascript
AngularJS 单元测试(二)详解
Sep 21 Javascript
详解angularjs利用ui-route异步加载组件
May 21 Javascript
js微信分享实现代码
Oct 11 Javascript
在vue中获取token,并将token写进header的方法
Sep 26 Javascript
基于Vue插入视频的2种方法小结
Apr 02 Javascript
JavaScript实现图片的放大缩小及拖拽功能示例
May 14 Javascript
详解Vue template 如何支持多个根结点
Feb 10 Javascript
使用Typescript和ES模块发布Node模块的方法
May 25 Javascript
Node.js中使用Buffer编码、解码二进制数据详解
Aug 16 #Javascript
Node.js中创建和管理外部进程详解
Aug 16 #Javascript
Node.js模块加载详解
Aug 16 #Javascript
JS遍历Json字符串中键值对先转成JSON对象再遍历
Aug 15 #Javascript
手机端网页点击链接触发自动拨打或保存电话的示例代码
Aug 15 #Javascript
Node.js中使用事件发射器模式实现事件绑定详解
Aug 15 #Javascript
Node.js中使用计时器定时执行函数详解
Aug 15 #Javascript
You might like
PHP json_encode() 函数详解及中文乱码问题
2015/11/05 PHP
Yii实现文章列表置顶功能示例
2016/10/18 PHP
PHP中md5()函数的用法讲解
2019/03/30 PHP
手把手教你自己写一个js表单验证框架的方法
2010/09/14 Javascript
可插入图片的TEXT文本框
2013/12/27 Javascript
JQuery表格拖动调整列宽效果(自己动手写的)
2014/09/01 Javascript
解决JS无法调用Controller问题的方法
2015/12/31 Javascript
JS数组去重(4种方法)
2017/03/27 Javascript
jQuery Tree Multiselect使用详解
2017/05/02 jQuery
JS实现上传图片实时预览功能
2017/05/22 Javascript
Vue的路由动态重定向和导航守卫实例
2018/03/17 Javascript
Vue.js实现开发购物车功能的方法详解
2019/02/22 Javascript
JS中call()和apply()的功能及用法实例分析
2019/06/28 Javascript
layui的select联动实现代码
2019/09/28 Javascript
vue瀑布流组件实现上拉加载更多
2020/03/10 Javascript
vue+element获取el-table某行的下标,根据下标操作数组对象方式
2020/08/07 Javascript
springboot+vue+对接支付宝接口+二维码扫描支付功能(沙箱环境)
2020/10/15 Javascript
Django中的CACHE_BACKEND参数和站点级Cache设置
2015/07/23 Python
Python中str is not callable问题详解及解决办法
2017/02/10 Python
利用Pandas读取文件路径或文件名称包含中文的csv文件方法
2018/07/04 Python
Python动态生成多维数组的方法示例
2018/08/09 Python
python2与python3的print及字符串格式化小结
2018/11/30 Python
python实现提取str字符串/json中多级目录下的某个值
2020/02/27 Python
python中逻辑与或(and、or)和按位与或异或(&、|、^)区别
2020/08/05 Python
详解python方法之绑定方法与非绑定方法
2020/08/17 Python
Vision Directa智利眼镜网:框架眼镜、隐形眼镜和名牌太阳眼镜
2016/11/23 全球购物
Expedia瑞典官网:预订度假屋、酒店、汽车租赁、机票等
2021/01/23 全球购物
Linux文件操作命令都有哪些
2015/02/27 面试题
九年级化学教学反思
2014/01/28 职场文书
驾驶员岗位职责
2014/01/29 职场文书
乡镇消防安全责任书
2014/07/23 职场文书
中学生运动会通讯稿大全
2014/09/18 职场文书
教师个人师德工作总结2015
2015/05/12 职场文书
医院消毒隔离制度
2015/08/05 职场文书
如何正确理解python装饰器
2021/06/15 Python
全网非常详细的pytest配置文件
2022/07/15 Python