让前端觉得如获神器的不是NodeJS能做网络编程,而是NodeJS能够操作文件。小至文件查找,大至代码编译,几乎没有一个前端工具不操作文件。换个角度讲,几乎也只需要一些数据处理逻辑,再加上一些文件操作,就能够编写出大多数前端工具。

小文件拷贝

使用NodeJS内置模块简单实现这个程序如下:

var fs=require('fs');
function copy(src, dst) {
fs.writeFileSync(dst, fs.readFileSync(src));
}
function main(argv) {
copy(argv[0], argv[1]);
} main(process.argv.slice(2));

以上程序使用fs.readFileSync从源路径读取文件内容,并使用fs.writeFileSync将文件内容写入目标路径。

process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。

大文件拷贝

上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完成拷贝。因此上边的程序需要改造如下。

var fs = require('fs');

function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
} function main(argv) {
copy(argv[0], argv[1]);
} main(process.argv.slice(2));

以上程序使用fs.createReadStream创建了一个源文件的只读数据流,并使用fs.createWriteStream创建了一个目标文件的只写数据流,并且用pipe方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶

Buffer(数据块)

JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。除了可以读取文件得到Buffer的实例外,还能够直接构造(后面nodeJS已更新,废弃了构造函数的方式)

  • Buffer.from(array) 返回一个新建的包含所提供的字节数组的副本的 Buffer
  • Buffer.from(arrayBuffer[, byteOffset [, length]]) 返回一个新建的与给定的 ArrayBuffer 共享同一内存的 Buffer
  • Buffer.from(buffer) 返回一个新建的包含所提供的 Buffer 的内容的副本的 Buffer
  • Buffer.from(string[, encoding]) 返回一个新建的包含所提供的字符串的副本的 Buffer
  • Buffer.alloc(size[, fill[, encoding]]) 返回一个指定大小的被填满的 Buffer 实例。 这个方法会明显地比 Buffer.allocUnsafe(size) 慢,但可确保新创建的 Buffer 实例绝不会包含旧的和潜在的敏感数据。
  • Buffer.allocUnsafe(size) 与 Buffer.allocUnsafeSlow(size) 返回一个新建的指定 size 的 Buffer,但它的内容必须被初始化,可以使用 buf.fill(0)或完全写满。

如果 size 小于或等于 Buffer.poolSize 的一半,则 Buffer.allocUnsafe() 返回的 Buffer 实例可能会被分配进一个共享的内部内存池。

Buffer与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。至于Buffer,更像是可以做指针操作的C语言数组。例如,可以用[index]方式直接修改某个位置的字节。而.slice方法也不是返回一个新的Buffer,而更像是返回了指向原Buffer中间的某个位置的指针。

Stream(数据流)

当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种Stream来提供对数据流的操作。

以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下:

var rs = fs.createReadStream(src);

rs.on('data', function (chunk) {
rs.pause();
doSomething(chunk, function () {
rs.resume();
});
}); rs.on('end', function () {
cleanUp();
});

以上代码给doSomething函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。

Stream基于事件机制工作,所有Stream的实例都继承于NodeJS提供的EventEmitter。

此外,我们也可以为数据目标创建一个只写数据流,示例如下:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst); rs.on('data', function (chunk) {
ws.write(chunk);
}); rs.on('end', function () {
ws.end();
});

我们把doSomething换成了往只写数据流里写入数据后,以上代码看起来就像是一个文件拷贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流内部的缓存会爆仓。我们可以根据.write方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据drain事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。因此代码可以改造如下:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst); rs.on('data', function (chunk) {
if (ws.write(chunk) === false) {
rs.pause();
}
}); rs.on('end', function () {
ws.end();
}); ws.on('drain', function () {
rs.resume();
});

以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场景很多,例如上边的大文件拷贝程序,NodeJS直接提供了.pipe方法来做这件事情,其内部实现方式与上边的代码类似。

File System(文件系统)

NodeJS通过fs内置模块提供对文件的操作。fs模块提供的API基本上可以分为以下三类:

  • 文件属性读写。

    其中常用的有fs.statfs.chmodfs.chown等等。

  • 文件内容读写。

    其中常用的有fs.readFilefs.readdirfs.writeFilefs.mkdir等等。

  • 底层文件操作。

    其中常用的有fs.openfs.readfs.writefs.close等等。

NodeJS最精华的异步IO模型在fs模块里有着充分的体现,例如上边提到的这些API都通过回调函数传递结果。以fs.readFile为例:

fs.readFile(pathname, function (err, data) {
if (err) {
// Deal with error.
} else {
// Deal with data.
}
});

Path(路径)

操作文件时难免不与文件路径打交道。NodeJS提供了path内置模块来简化路径相关操作,并提升代码可读性。以下分别介绍几个常用的API。

path.normalize

将传入的路径转换为标准路径,具体讲的话,除了解析路径中的...外,还能去掉多余的斜杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用该方法保证路径的唯一性。以下是一个例子:

  var cache = {};

  function store(key, value) {
cache[path.normalize(key)] = value;
} store('foo/bar', 1);
store('foo//baz//../bar', 2);
console.log(cache); // => { "foo/bar": 2 }

坑出没注意: 标准化之后的路径里的斜杠在Windows系统下是\,而在Linux系统下是/。如果想保证任何系统下都使用/作为路径分隔符的话,需要用.replace(/\\/g, '/')再替换一下标准路径。

path.join

将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系统下正确使用相应的路径分隔符。以下是一个例子

 path.join('foo/', 'baz/', '../bar'); // => "foo/bar"

遍历目录

遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS文件时,就需要遍历整个目录。

递归算法

遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通过不断缩小问题的规模来解决问题。以下示例说明了这种方法。

function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 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);
}
});
}

可以看到,该函数以某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断和处理。

异步遍历

如果读取目录或读取文件状态时使用的是异步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编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。我们常用的文本编码有UTF8GBK两种,并且UTF8文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。

BOM的移除

BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符("\uFEFF"),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:

    Bytes      Encoding
----------------------------
FE FF UTF16BE
FF FE UTF16LE
EF BB BF UTF8

因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误。因此,使用NodeJS读取文本文件时,一般需要去掉BOM。例如,以下代码实现了识别和去除UTF8 BOM的功能。

function readText(pathname) {
var bin = fs.readFileSync(pathname); if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
bin = bin.slice(3);
} return bin.toString('utf-8');
}

GBK转UTF8

NodeJS支持在读取文本文件时,或者在Buffer转换为字符串时指定文本编码,但遗憾的是,GBK编码不在NodeJS自身支持范围内。因此,一般我们借助iconv-lite这个三方包来转换编码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。

var iconv = require('iconv-lite');

function readGBKText(pathname) {
var bin = fs.readFileSync(pathname); return iconv.decode(bin, 'gbk');
}

nodeJS文件操作的更多相关文章

  1. nodejs 文件操作

    前言: nodejs 自带的文件操作的模块  fs 就是对文件的增删查改: 就像我们用的服务器,我们没有办法在运行的文件上进行一直的修改,因为他不向浏览器,刷新后我们的文件会自己修改: 如果想要更改我 ...

  2. nodejs文件操作模块FS(File System)常用函数简明总结(转)

    件系统操作相关的函数挺多的.首先可以分为两大类. 一类是异步+回调的. 一类是同步的. 在这里只对异步的进行整理,同步的只需要在函数名称后面加上Sync即可 1. 首先是一类最常规的读写函数,函数名称 ...

  3. nodejs 文件操作模块 fs

    const fs=require("fs"); //文件操作 //创建目录 ./ 代表当前目录 ../ 代表上级目录fs.mkdir('./test',function(err){ ...

  4. nodejs文件操作笔记

    nodejs添加了流的概念,通过流操作文件如行云流水,比早前便利畅快多了. 先来第一个例子,我们建一个stream.js文件,里面内容如下: var fs = require("fs&quo ...

  5. NodeJS学习之文件操作

    NodeJS -- 文件操作 Buffer(数据块) JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作 ...

  6. 【nodejs学习】1.文件操作

    1.小文件拷贝,使用nodejs内置模块 var fs = require('fs'); function copy(src, dst){ fs.writeFileSync(dst, fs.readF ...

  7. Nodejs学习笔记(4) 文件操作 fs 及 express 上传

    目录 参考资料 1. fs 模块 1.1 读取文件fs.readFile 1.2 写入文件fs.writeFile 1.3 获取文件信息fs.stat 1.4 删除文件fs.unlink 1.5 读取 ...

  8. nodejs零基础详细教程2:模块化、fs文件操作模块、http创建服务模块

    第二章  建议学习时间4小时  课程共10章 学习方式:详细阅读,并手动实现相关代码 学习目标:此教程将教会大家 安装Node.搭建服务器.express.mysql.mongodb.编写后台业务逻辑 ...

  9. nodejs笔记之文件操作

    文件操作包含: 读取文件 写入文件 删除文件 创建目录 删除目录 读取文件: // 异步操作 var fs = require("fs"); fs.readFile(". ...

随机推荐

  1. 剑指offer(26)二叉搜索树与双向链表

    题目描述 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表.要求不能创建任何新的结点,只能调整树中结点指针的指向. 题目分析 要生成排序的双向列表,那么只能是中序遍历,因为中序遍历才能从小到 ...

  2. 论文笔记:Improving Deep Visual Representation for Person Re-identification by Global and Local Image-language Association

    Improving Deep Visual Representation for Person Re-identification by Global and Local Image-language ...

  3. 数组中的stdClass Object如何访问

    使用print_r($data)输出结果为 Array ( [0] => stdClass Object ( [color_item_no] => 1 [color_name] => ...

  4. js 数组的拷贝

    在js中,数组Array是引用类型,直接将数组赋值给一个变量名,二者所指向的地址是一样的. 所以直接复制数组会产生意想不到的结构. 要想解决拷贝一个数组但是对副本的修改不影响原来的数组,有以下方式: ...

  5. 全面解读第四代基因测序技术Oxford Nanopore--转载

    纳米孔测序技术(又称第四代测序技术)是最近几年兴起的新一代测序技术.目前测序长度可以达到150kb.这项技术开始于90年代,经历了三个主要的技术革新:一.单分子DNA从纳米孔通过:二.纳米孔上的酶对于 ...

  6. 初学者易上手的SSH-struts2 03数据封装

    这一章我们一样来获取数据,看看与上一章有什么不同吧.数据封装也有三种方式.下面我们来一一介绍. 第一种:属性封装. 类就用LoginAction吧.里面有两属性,name,pwd.给这两个属性写上ge ...

  7. oracle数据库 concat 与 ||

    1.简述: 通过几条sql语句来引入此问题 先新建一张表,插入几条数据,如下: CREATE TABLE tab1 (col1 VARCHAR2(6), col2 CHAR(6), col3 VARC ...

  8. DFS 之 全排列

    题目描述输出自然数1到n所有不重复的排列,即n的全排列,要求所产生的任一数字序列中不允许出现重复的数字. 我们可以模拟出n个盒子和n张卡片,我们需要将n张卡片分别放到n个盒子里,且每个盒子只能放1张卡 ...

  9. Android studio 中,遇到报错:ERROR: x86_64 emulation currently requires hardware acceleration! CPU acceleration status:HAXM must be updated(version 1.1.1<6.0.1)的解决方法

    在 Android SDK 的安装目录下找到intelhaxm-android.exe安装即可.忘记目录位置直接在“计算机”中搜索,如果没有,返回Android studio 的“设置”,找到 这个图 ...

  10. 2015-10-07 jQuery2

    jQuery (2) 四. 过滤选择器 1.  $("input[type='button']").val(“中国”)   //所有设置type=button的input,其val ...