Node.js学习

学习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按照如下方式来运行

  1. 加载代码,从开始执行到最后一行,在命令行输出Now watching target.txt for changes.....
  2. 由于调用了fs.watch,所以node.js不会退出
  3. 它等待着fs模块监听目标文件的变化
  4. 当目标文件发生变化时,执行回调函数
  5. 程序继续等待,继续监听,还不能退出

事件循环会一直持续下去,直到没有任何代码需要执行,没有任何事件需要等待,或程序因为其他因素退出。比如程序运行时候发生错误抛出异常,异常有没有被正确捕获到,通常会导致进程 退出。

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事件。这个阶段不要使用同步的文件操作,否则会阻塞其他的事件。

Node.js学习

上一篇:数据库事务之不可重复读


下一篇:js 时间与时间戳转换