学习node之前,首先要安装node,去node官网安装比较node.js 8的稳定版本,然后学习
1.文件操作
在工作中会遇到文件操作,比如读文件,写文件,文件重命名,删除文件,以文件操作作为学习Node.js的起点。接下来会创建一些实用的异步操作文件的工具。
1.1 Node.js事件循环编程
1.1.1 监听文件变化
打开命令行窗口,创建目录filesystem,在该目录下面新建target.txt文件,
mkdir filesystem
cd filesystem
touch target.txt
在filesystem目录下新建watcher.js文件,文件内容如下:
‘use strict‘;
const fs = require(‘fs‘);
fs.watch(‘target.txt‘, () => { console.log(‘File Changed!‘) });
console.log(‘Now watching target.txt for changes.....‘);
上面代码,第一行的‘use strict‘表示让代码在严格模式下运行。严格模式是ES5的新特性。
require()函数用于引入Node.js模块并将这个模块作为返回值。在本例中,执行require(‘fs‘)是为了引入Node.js内置的文件模块。
在Node.js中,模块是一段独立的Javascript代码,它提供的功能可以用于其他地方。require()的返回值通常是Javascript对象或者是函数
。模块还可以依赖别的模块,类似于其他模块语言中库的概念,其他编程语言中的库也可以是import或#include其他库。然后调用fs模块的watch()方法,这个方法接收2个参数,一个是文件路径,一个是文件变化时需要执行的回调函数。在Javascript中,函数是一等公民,也就是说,函数可以被赋值给变量,或者作为参数传递给别的参数。
接着在命令行试着运行,使用node启动这个监听程序
node watcher.js
Now watching target.txt for changes.....
程序启动之后,Node.js会安静地等待目标文件内容的变化。修改target.txt文件的内容,看命令行的输出,看到输出了File Changed!,然后监听程序会继续等待文件内容的变化。
如果看到了几条重复的输出消息,并不是代码出现了bug,原因与操作系统对文件变化的处理方式有关。
node watcher.js
Now watching target.txt for changes.....
File Changed!
File Changed!
File Changed!
File Changed!
1.1.2 看得见的事件循环
上一节的例子展示了node.js事件循环的工作。Node.js按照如下方式来运行
- 加载代码,从开始执行到最后一行,在命令行输出Now watching target.txt for changes.....
- 由于调用了fs.watch,所以node.js不会退出
- 它等待着fs模块监听目标文件的变化
- 当目标文件发生变化时,执行回调函数
- 程序继续等待,继续监听,还不能退出
事件循环会一直持续下去,直到没有任何代码需要执行,没有任何事件需要等待,或程序因为其他因素退出。比如程序运行时候发生错误抛出异常,异常有没有被正确捕获到,通常会导致进程 退出。
1.1.3 接收命令行参数
改进监听程序,让它能够接收参数,在参数中指定我们要监听哪个文件。用到process全局对象,还有如何捕获异常,如下:
‘use strict‘;
const fs = require(‘fs‘);
const filename = process.argv[2];
if (!filename) {
throw Error(‘A file to watch must be specified!‘)
}
fs.watch(filename, () => { console.log(‘File ${filename} Changed!‘) });
console.log(`Now watching ${filename} for changes.....`);
按照下面方式运行
node watcher.js target.txt
Now watching target.txt for changes.....
输出的内容和上一小节一致,通过process.argv访问命令行输入的参数。argv是argument vector的简写,它的值是数组,其中输出的前2项分别是node 和target.txt的绝对路径,数组的第3项就是目标文件的文件名target.txt。
‘File ${filename} Changed!‘)输出信息是由反引号(``)包裹起来的字符串,称为模板字符串。
如果输入node watcher.js命令,就会抛出异常退出。所以未捕获的异常都会导致Node.js执行进程退出。错误信息一般包含抛错的文件名、抛错的行数和具体的错误位置。
node watcher.js
watcher.js:5
throw Error(‘A file to watch must be specified!‘)
^
Error: A file to watch must be specified!
进程是node.js中非常重要的概念,在开发中常见做法是不同的工作放在不同的独立进程中执行,而不是所有代码都塞进一个巨无霸Node.js程序里,下一节学习如何在Node.js中创建进程。
1.2 创建子进程
继续优化监听程序,让它在监听到文件变化后创建一个子进程,再用这个子进程执行系统该命令,在此过程中,会接触到child-process模块,Node.js的开发模式和一些内置类,还会学习如何用流进行数据传送。
编辑如下代码,代码会执行ls命令并加上-l和-h参数,这样就能看到目标文件的修改时间。
‘use strict‘;
const fs = require(‘fs‘);
const spawn = require(‘child_process‘).spawn;
const filename = process.argv[2];
if (!filename) {
throw Error(‘A file to watch must be specified!‘)
}
fs.watch(filename, () => {
const ls = spawn(‘ls‘, [‘-l‘, ‘-h‘, filename]);
ls.stdout.pipe(process.stdout);
});
console.log(`Now watching ${filename} for changes.....`);
执行下面命令运行它
node watcher.js target.txt
Now watching target.txt for changes.....
当修改target.txt文件后,监听程序会输出类似这样的信息,有用户名,用户组,文件属性信息
-rw-r--r-- 1 ff ff 6 7月 13 15:48 target.txt
代码的开始部分有新的require()语句,require(‘child_process‘)语句将返回child_process模块。目前我们只关心其中的spawn()方法,所以把spawn()方法赋值给一个常量且暂时忽略模块中的其他功能,在javascript中,函数是一等公民,可以直接赋值给另外一个变量。
const spawn = require(‘child_process‘).spawn;
spawn()的第一个参数是需要执行命令的名称,在本例中就是ls。第二个参数是命令行的参数数组,包括ls命令本身的参数和目标文件名。
spawn()返回的对象是ChildProcess。它的stdin、stdout、stderr属性都是Stream,可以用作输入和输出。使用pipe()方法把子进程的输出内容直接传送到标准输出流。
有些场景下,我们需要读取输出的数据而不是直接传送,怎么做呢?
1.3 使用EventEmitter获取数据
EventEmitter是Node.js中非常重要的一个类,可以通过它触发事件或者响应事件。Node.js中的很多对象都继承自EventEmitter,例如上一节提到的Stream类。
修改上节的例子,通过监听stream的事件来获取子进程的输出内容。如下:
‘use strict‘;
const fs = require(‘fs‘);
const spawn = require(‘child_process‘).spawn;
const filename = process.argv[2];
if (!filename) {
throw Error(‘A file to watch must be specified!‘)
}
fs.watch(filename, () => {
const ls = spawn(‘ls‘, [‘-l‘, ‘-h‘, filename]);
let output = ‘‘;
ls.stdout.on(‘data‘, chunk => output += chunk);
ls.on(‘close‘, () => {
const parts = output.split(/\s+/);
console.log(parts[0], parts[4], parts[8]);
})
});
console.log(`Now watching ${filename} for changes.....`);
执行命令,然后修改target.txt文件内容,控制台输出内容如下:
node watcher.js target.txt
Now watching target.txt for changes.....
-rw-r--r-- 12 target.txt
这个新的回调函数,会像之前一样被调用,它会创建一个子进程并把子进程赋值给ls变量。函数内也会声明output变量,用于把子进程输出的内容暂存起来。
接下来添加事件监听函数。当特定类型的事件发生时,这个监听函数就会被调用。Stream类继承自EventEmitter,所以能够监听到子进程标准输出流的事件
ls.stdout.on(‘data‘, chunk => output += chunk);
上面这行代码的信息量比较大,我们拆看来看。
这里的箭头函数接收chunk参数。on()方法用于给指定事件添加事件监听函数,本例中监听的是data事件,因为我们要获得输出流的数据。
事件发生后,可以通过回调函数的参数获取跟事件相关的信息,比如本例中的data时间会将buffer对象作为参数传给回调函数,然后每拿到一部分数据,我们将把这个参数里的数据添加到output变量。
Node.js中使用Buffer描述二进制数据。它指向一段内存中的数据,这个数据由Node.js内核管理,而不在javascript引擎中。Buffer不能修改,并且需要解码和编码的过程才能装换成JavaScript字符串。
在javascript里,把非string值添加到string中,都会隐式调用对象的toString()方法。具体到Buffer对象,当它跟一个string相加时,会把这个二进制数据复制到Node.js堆栈中,然后使用默认方式(UTF-8)编码。
把数据从二进制复制到Node.js的操作非常耗时,所以尽管string操作更加敏捷,但还是应该尽可能直接操作Buffer。在这个例子中,由于数据量很小,相应的耗时也很少,影响不大。但是希望大家今后在使用Buffer时,脑子有这个印象,
尽可能直接操作Buffer
。
child_process类也继承自EventEmitter,也可以给它添加事件监听函数
ls.on(‘close‘, () => {
const parts = output.split(/\s+/);
console.log(parts[0], parts[4], parts[8]);
})
当子进程退出时,会触发close事件。回调函数将数据按照空白符切割,然后打印出第1,5,9个字段,这3个字段分别对应权限、大小和文件名。
1.4异步读/写文件
Node.js中有多种读/写文件的方式,其中最简单直接的是一次性读取或写入整个文件,这种方式对小文件很有效。另外的方式通过Stream 读/写流和使用Buffer存储内容,下面是一次性读/写整个文件的例子
新建文件read-simple.js文件,如下:
‘use strict‘
const fs = require(‘fs‘);
fs.readFile(‘target.txt‘, (err, data) => {
if (err) {
throw err;
}
console.log(data.toString())
})
执行命令
node read-simple.js
aaafffssssss
ffffffffeeff
注意readFile()的回调函数的第一个参数是err,如果readFile()执行成功,则err的值为null。如果readFile()执行失败,则err会是一个Error对象,这是Node.js中统一的错误处理方式,尤其是内置模块一定会按这种方式处理错误。本例中,如果有错误,我们直接就将错误抛出,未捕获的异常会导致Node.js直接中断退出。
回调函数的第2个参数是Buffer对象,如上节例子中的那样。
一次写入整个文件的做法也是类似的, 如下:
‘use strict‘
const fs = require(‘fs‘);
fs.writeFile(‘target.txt‘, ‘hello world‘, (err) => {
if (err) {
throw err;
}
console.log(‘File saved!‘);
})
这段代码的功能是将hello world写入target.txt文件(如果这个文件不存在,则创建一个新的;如果已经存在,则覆盖它)。如果有任何因素导致写入失败,则err参数会包含一个Error实例对象。
1.4.1 创建读/写流
分别用fs.createReadStream()和fs.createWriteStream()来创建读/写流。
‘use strict‘
require(‘fs‘).createReadStream(process.argv[2]).pipe(process.stdout);
执行下面命令,输出结果如下:
node cat.js target.txt
hello world
也可以通过监听文件流的data事件来达到同样效果,如下
‘use strict‘
require(‘fs‘).createReadStream(process.argv[2])
.on(‘data‘, chunk => process.stdout.write(chunk))
.on(‘error‘, err => process.stderr.write(`ERROR: ${err.message}\n`));
这里使用process.stdout.write()输出数据,替换原来的console.log。输入数据chunk中已经包含文件中的所有换行符,因此不再需要console.log来增加换行。
更更变的是.on()返回的是emitter对象,因此可以直接在后面链式得添加事件处理函数。
当使用EventEmitter时,最方便的错误处理方式就是直接监听它的error事件。现在人为触发错误,看看输出。
node read-stream.js target.tx
ERROR: ENOENT: no such file or directory, open ‘\ilesystem\target.tx‘
由于监听了error事件,所以Node.js调用了错误监听函数(并且正常退出)。如果没有监听error事件,并且恰好发生了运行错误,那么node.js会直接抛出异常,然后导致进程异常退出。
1.4.2 使用同步文件操作阻塞事件循环
到目前为至,我们讨论的文件操作方法都是异常的,他们都是默默地在后台履行I/O职责,只有事件发生时才会调用回调函数,这是较妥当的I/O处理方式。
同时,fs模块中的很多方法也有相应的同步版本,这些同步方法大多以*Sync结尾,比如readFileSync。
当调用同步方法时,Nodejs进程会被堵塞,直到I/O处理完毕。也就是说,这时Node.js不会执行其他代码,不会调用任何回调函数,不会触发 任何事件,会完全停止下来,等待操作完成。如下代码:
‘use strict‘
const fs = require(‘fs‘);
const data = fs.readFileSync(‘target.txt‘);
process.stdout.write(data.toString());
1.4.3 文件操作的其他方法
Node.js的fs模块还有方法,比如可以使用copy()方法复制文件,使用unlink()方法删除文件,可以使用chmod方法变更权限,可以使用mkdir()方法创建文件夹。
这些函数的用法类似,他们的回调函数都接受相同的参数。
1.5 Node.js程序运行的2个阶段
Node.js运行的2个阶段
第一个阶段是初始化阶段,代码会做一些准备工作,导入依赖的库,读取配置参数等。如果这个阶段发生了错误,没有太多办法,最好是尽早抛出错误并退出。
第二个阶段是代码执行阶段,事件循环机制开始工作。相当多的Node.js应用是网络应用,也就是会建立连接、发送请求或者等待其他类型的I/O事件。这个阶段不要使用同步的文件操作,否则会阻塞其他的事件。