1 Stream(数据流)
当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到
数据流。NodeJS中通过各种 Stream
来提供对数据流的操作。
(1)为数据来源创建一个只读数据流:
var rs = fs.createReadStream(src); // 从src读取文件,
返回一个新的可读流对象
rs.on(‘data‘, function (chunk)
{ //
触发‘data‘事件
rs.pause();
//暂停触发‘data‘事件
doSomething(chunk, function ()
{
rs.resume();
//恢复触发‘data‘事件
});
});
rs.on(‘end‘, function ()
{
cleanUp();
});
(2)为数据目标创建一个只写数据流:
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 () {
//发生在write()方法被调用并返回false之后。此事件被触发说明内核缓冲区已空,再次写入是安全的。
rs.resume();
});
以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场
景很多,例如上边的大文件拷贝程序,NodeJS直接提供了
.pipe 方法来做这件事情,其内部实
现方式与上边的代码类似。
2 File System(文件系统)
NodeJS通过 fs 内置模块提供对文件的操作。fs 模块提供的API基本上可以分为以下三类:
? 文件属性读写。
其中常用的有
fs.stat、fs.chmod、fs.chown 等等。
? 文件内容读写。
其中常用的有
fs.readFile、fs.readdir、fs.writeFile、fs.mkdir 等等。
? 底层文件操作。
其中常用的有
fs.open、fs.read、fs.write、fs.close 等等。
NodeJS最精华的异步IO模型在 fs 模块里有着充分的体现,例如上边提到的这些API都通过回调
函数传递结果。以 fs.readFile
为例:
fs.readFile(pathname, function (err, data) {
if (err)
{
// Deal with error.
} else {
// Deal with
data.
}
});
如上边代码所示,基本上所有 fs
模块API的回调参数都有两个。第一个参数在有错误发生时等
于异常对象,第二个参数始终用于返回API方法执行结果。
此外,fs
模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作
更方便时的情况。同步API除了方法名的末尾多了一个 Sync
之外,异常对象与执行结果的传递
方式也有相应变化。同样以 fs.readFileSync 为例:
try {
var data =
fs.readFileSync(pathname);
// Deal with data.
} catch (err)
{
// Deal with error.
}
3 Path(路径)
操作文件时难免不与文件路径打交道。NodeJS提供了 path
内置模块来简化路径相关操作,并提
升代码可读性。
(1) 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系统下是 \,而在*nix系统下是 /。如果想保证任何
系统下都使用 / 作为路径分隔符的话,需要用
.replace(/\\/g, ‘/‘) 再替换一下标准
路径。
(2)
path.join:将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系
统下正确使用相应的路径分隔符。以下是一个例子:
path.join(‘foo/‘,
‘baz/‘, ‘../bar‘); // => "foo/bar"
(3) path.extname:该方法返回路径中的文件扩展名,即路径最低一级的目录中‘.‘字符后的任何字符串。如果路径最低一级的目录中没有‘.‘
或者只有‘.‘,那么该方法返回一个空字符串。
当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。以下是一个例子:
path.extname(‘foo/bar.js‘);
// => ".js"
4
遍历目录
遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS
文件时,就需要遍历整个目录。
(1)遍历算法:
目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个
节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完
成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是
A
> B > D > E > C > F。
A
/ \
B C
/ \ \
D
E F
(2) 同步遍历:
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);
}
});
}
(3) 异步遍历:
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));
});
}
5 文本编码:
(1) 常用的文本编码有 UTF8 和 GBK 两种,并且 UTF8
文件还可能带有BOM。在读取不同
编码的文本文件时,需要将文件内容转换为JS使用的 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‘);
}
(2) 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‘);
}
(3)
单字节编码:即使一个文本文件中有中文等字符,如果我们需要处理的字符仅在ASCII0~128范围
内,比如除了注释和字符串以外的JS代码,我们就可以统一使用单字节编码来读取文件,不用关
心文件的实际编码是GBK还是UTF8。
1.
GBK编码源文件内容:
var foo = ‘中文‘;
2. 对应字节:
76 61 72 20 66 6F 6F 20 3D 20 27
D6 D0 CE C4 27 3B
3. 使用单字节编码读取后得到的内容:
var foo = ‘{乱码}{乱码}{乱码}{乱码}‘;
4.
替换内容:
var bar = ‘{乱码}{乱码}{乱码}{乱码}‘;
5. 使用单字节编码保存后对应字节:
76 61 72 20 62
61 72 20 3D 20 27 D6 D0 CE C4 27 3B
6. 使用GBK编码读取后得到内容:
var bar =
‘中文‘;
这里的诀窍在于,不管大于0xEF的单个字节在单字节编码下被解析成什么乱码字符,使用同样
的单字节编码保存这些乱码字符时,背后对应的字节保持不变。
NodeJS中自带了一种 binary
编码可以用来实现这个方法,因此在下例中,我们使用这种编码
来演示上例对应的代码该怎么写。
function replace(pathname)
{
var str = fs.readFileSync(pathname, ‘binary‘);
str =
str.replace(‘foo‘, ‘bar‘);
fs.writeFileSync(pathname, str,
‘binary‘);
}
总结:
? 学好文件操作,编写各种程序都不怕。
? 如果不是很在意性能,fs 模块的同步API能让生活更加美好。
?
需要对文件读写做到字节级别的精细控制时,请使用 fs 模块的文件底层操作API。
? 不要使用拼接字符串的方式来处理路径,使用 path 模块。
?
掌握好目录遍历和文件编码处理技巧,很实用。
进程管理
1 任何一个进程都有启动进程时使用的命令行参数,有标准输入标准输出,有运行权限,有运行环
境和运行状态。在NodeJS中,可以通过 process
对象感知和控制NodeJS自身进程的方方面面。
另外需要注意的是,process
不是内置模块,而是一个全局对象,因此在任何地方都可以直接
使用。
在NodeJS中可以通过 process.argv 获取命令行参数。但是比较意外的是,node 执行程序路
径和主模块文件路径固定占据了
argv[0] 和 argv[1] 两个位置,而第一个命令行参数从
argv[2] 开始。为了让 argv
使用起来更加自然,可以按照以下方式处理。
function main(argv) {
//
...
}
main(process.argv.slice(2));
2 使用 child_process 模块可以创建和控制子进程。该模块提供的API中最核心的是
.spawn,
其余API都是针对特定使用场景对它的进一步封装,算是一种语法糖。
3 cluster 模块是对 child_process 模块的进一步封装,专用于解决单进程NodeJS
Web服务器
无法充分利用多核CPU的问题。使用该模块可以简化多进程服务器程序的开发,让每个核上运行
一个工作进程,并统一通过主进程监听端口和分发请求。
4 NodeJS程序的标准输入流(stdin)、一个标准输出流(stdout)、一个标准错误流(stderr)分别
对应
process.stdin、process.stdout 和
process.stderr,第一个是只读数据流,后
边两个是只写数据流,对它们的操作按照对数据流的操作方式即可。
5
> var t = new Date(); #=> Tue Mar 25 2014 08:41:27
GMT+0000 (UTC)
> var k = new Date(); #=> Tue Mar 25 2014 08:41:58 GMT+0000 (UTC)
> k - t #=> 30816 (毫秒)