让前端觉得如获神器的不是NodeJS能做网络编程,而是NodeJS能够操作文件。小至文件查找,大至代码编译,几乎没有一个前端工具不操作文件。换个角度讲,几乎也只需要一些数据处理逻辑,再加上一些文件操作,就能够编写出大多数前端工具。本章将介绍与之相关的NodeJS内置模块。
NodeJS提供了基本的文件操作API,但是像文件拷贝这种高级功能就没有提供,因此我们先拿文件拷贝程序练手。与<code>copy</code>命令类似,我们的程序需要能接受源文件路径与目标文件路径两个参数。
一、文件拷贝
1、小文件拷贝
我们使用NodeJS内置的<code>fs</code>模块简单实现这个程序如下
以上程序使用<code>fs.readFileSync</code>从源路径读取文件内容,并使用<code>fs.writeFileSync</code>将文件内容写入目标路径。
注意:<code>process</code>是一个全局变量,可通过<code>process.argv</code>获得命令行参数。由于<code>argv[0]</code>固定等于NodeJS执行程序的绝对路径,<code>argv[1]</code>固定等于主模块的绝对路径,因此第一个命令行参数从<code>argv[2]</code>这个位置开始。
2、大文件拷贝
上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完成拷贝。因此上边的程序需要改造如下。
以上程序使用<code>fs.createReadStream</code>创建了一个源文件的只读数据流,并使用<code>fs.createWriteStream</code>创建了一个目标文件的只写数据流,并且用<code>pipe</code>方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。
二、API
我们先大致看看NodeJS提供了哪些和文件操作有关的API。这里并不逐一介绍每个API的使用方法,官方文档已经做得很好了。
1、Buffer(数据块)
官方文档: http://nodejs.org/api/buffer.html
JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与<code>String</code>对等的全局构造函数<code>Buffer</code>来提供对二进制数据的操作。除了可以读取文件得到<code>Buffer</code>的实例外,还能够直接构造
<code> Buffer</code>与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。至于<code>Buffer</code>,更像是可以做指针操作的C语言数组。例如,可以用<code>[index]</code>方式直接修改某个位置的字节。
而.slice方法也不是返回一个新的Buffer,而更像是返回了指向原Buffer中间的某个位置的指针,如下所示。
因此对<code>.slice</code>方法返回的<code>Buffer</code>的修改会作用于原<code>Buffer</code>,例如:
也因此,如果想要拷贝一份<code>Buffer</code>,得首先创建一个新的<code>Buffer</code>,并通过<code>.copy</code>方法把原<code>Buffer</code>中的数据复制过去。这个类似于申请一块新的内存,并把已有内存中的数据复制过去。以下是一个例子。
总之,<code>Buffer</code>将JS的数据处理能力从字符串扩展到了任意二进制数据。
2、Stream(数据流)
官方文档: http://nodejs.org/api/stream.html
当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种<code>Stream</code>来提供对数据流的操作。
以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下
注意:<code>Stream</code>基于事件机制工作,所有<code>Stream</code>的实例都继承于NodeJS提供的EventEmitter。
上边的代码中<code>data</code>事件会源源不断地被触发,不管<code>doSomething</code>函数是否处理得过来。代码可以继续做如下改造,以解决这个问题
以上代码给<code>doSomething</code>函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。
此外,我们也可以为数据目标创建一个只写数据流,示例如下:
我们把<code>doSomething</code>换成了往只写数据流里写入数据后,以上代码看起来就像是一个文件拷贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流内部的缓存会爆仓。我们可以根据<code>.write</code>方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据<code>drain</code>事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。因此代码可以改造如下:
以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场景很多,例如上边的大文件拷贝程序,NodeJS直接提供了<code>.pipe</code>方法来做这件事情,其内部实现方式与上边的代码类似。
3、File System(文件系统)
官方文档: http://nodejs.org/api/fs.html
NodeJS通过<code>fs</code>内置模块提供对文件的操作。<code>fs</code>模块提供的API基本上可以分为以下三类:
文件属性读写。
其中常用的有<code>fs.stat</code>、<code>fs.chmod</code>、<code>fs.chown</code>等等。
文件内容读写。
其中常用的有<code>fs.readFile</code>、<code>fs.readdir</code>、<code>fs.writeFile</code>、<code>fs.mkdir</code>等等。
底层文件操作。
其中常用的有<code>fs.open</code>、<code>fs.read</code>、<code>fs.write</code>、<code>fs.close</code>等等。
NodeJS最精华的异步IO模型在<code>fs</code>模块里有着充分的体现,例如上边提到的这些API都通过回调函数传递结果。以<code>fs.readFile</code>为例
如上边代码所示,基本上所有<code>fs</code>模块API的回调参数都有两个。第一个参数在有错误发生时等于异常对象,第二个参数始终用于返回API方法执行结果。
此外,<code>fs</code>模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作更方便时的情况。同步API除了方法名的末尾多了一个<code>Sync</code>之外,异常对象与执行结果的传递方式也有相应变化。同样以<code>fs.readFileSync</code>为例:
<code> fs</code>模块提供的API很多,需要时请自行查阅官方文档
4、Path(路径)
官方文档: http://nodejs.org/api/path.html
操作文件时难免不与文件路径打交道。NodeJS提供了<code>path</code>内置模块来简化路径相关操作,并提升代码可读性。以下分别介绍几个常用的API。
path.normalize:将传入的路径转换为标准路径,具体讲的话,除了解析路径中的<code>.</code>与<code>..</code>外,还能去掉多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。
注意: 标准化之后的路径里的斜杠在Windows系统下是<code>\</code>,而在Linux系统下是<code>/</code>。如果想保证任何系统下都使用<code>/</code>作为路径分隔符的话,需要用<code>.replace(/\\/g, '/')</code>再替换一下标准路径。
path.extname:当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用
<code> path</code>模块提供的其余方法也不多,稍微看一下官方文档就能全部掌握。
三、遍历目录
遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS文件时,就需要遍历整个目录。
1、递归算法
遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通过不断缩小问题的规模来解决问题。
陷阱: 使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。
2、遍历算法
目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。
深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。
先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是<code>A > B > D > E > C > F</code>。
3、同步遍历
了解了必要的算法后,我们可以简单地实现以下目录遍历函数。
可以看到,该函数以某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断和处理。因此假设有以下目录:
4、异步遍历
如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理完全相同。<code>travel</code>函数的异步版本如下。
四、文本编码
使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。我们常用的文本编码有<code>UTF8</code>和<code>GBK</code>两种,并且<code>UTF8</code>文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的<code>UTF8</code>编码字符串后才能正常处理。