在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。

在Node上运行的JavaScript相比其他后端开发语言有何优势?

最大的优势是借助JavaScript天生的事件驱动机制加V8高性能引擎,使编写高性能Web服务轻而易举。

编写node程序

我们编写的JavaScript代码将不能在浏览器环境中执行了,而是在Node环境中执行

1
C:\Users\86178\Desktop\Study\html+css+js_code>node hello.js

在命令行模式下,可以执行node进入Node交互式环境

看到>是在Node交互式环境下,按两次ctrl + c退出。在Node交互式环境下,我们可以输入JavaScript代码并立刻执行。如果在.js的文件中,想看到结果就要用console.log()打印出来。

可以给Nodejs传递一个参数,让Node直接为所有js文件开启严格模式:
node --use_strict calc.js
后续代码,如无特殊说明,我们都会直接给Node传递--use_strict参数来开启严格模式。

模块

最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Node内置的模块和来自第三方的模块。

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。

模块的名字就是文件名(去掉.js后缀)

require函数引入模块

1
2
3
4
5
6
7
8
9
10
11
12
//hello.js
'use strict';
var s = 'Hello';
function greet(name) {
console.log(s + ', ' + name + '!');
}
module.exports = greet;
//把函数greet作为模块的输出暴露出去,这样其他模块就可以使用greet函数了
module.exports = {
hello: hello,
greet: greet
}

其他模块引入:

1
2
3
var greet = require('./hello'); // 不要忘了写相对目录!
...
greet(s); // Hello, Michael!

CommonJS规范

这种模块加载机制被称为CommonJS规范。它们内部各自使用的变量名和函数名都互不冲突。

实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。如果我们把一段JavaScript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。

如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值;

如果要输出一个函数或数组,必须直接对module.exports对象赋值。

基本模块

因为Node.js是运行在服务区端的JavaScript环境,服务器程序和浏览器程序相比,最大的特点是没有浏览器的安全限制了,而且,服务器程序必须能接收网络请求,读写文件,处理二进制内容,所以,Node.js内置的常用模块就是为了实现基本的服务器功能。这些模块在浏览器环境中是无法被执行的,因为它们的底层代码是用C/C++在Node.js运行环境中实现的。

global

JavaScript有且仅有一个全局对象,在浏览器中,叫window对象。而在Node.js环境中,也有唯一的全局对象,叫global

process

也是Node.js提供的一个对象,它代表当前Node.js进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> process === global.process;
true
> process.version;
'v12.18.3'
> process.platform;
'win32'
> process.arch;
'x64'
> process.cwd();
'C:\\Users\\86178'
> process.chdir('desktop/study');
undefined
> process.cwd();
'C:\\Users\\86178\\desktop\\study'

JavaScript程序是由事件驱动执行的单线程模型,Node.js也不例外。Node.js不断执行响应事件的JavaScript函数,直到没有任何响应事件的函数可以执行时,Node.js就退出了。

如果我们想要在下一次事件响应中执行代码,可以调用process.nextTick():

1
2
3
4
5
6
7
// test.js

// process.nextTick()将在下一轮事件循环中调用:
process.nextTick(function () {
console.log('nextTick callback!');
});
console.log('nextTick was set!');

用Node执行上面的代码node test.js,你会看到,打印输出是:

1
2
nextTick was set!
nextTick callback!

这说明传入process.nextTick()的函数不是立刻执行,而是要等到下一次事件循环。

Node.js进程本身的事件就由process对象来处理。如果我们响应exit事件,就可以在程序即将退出时执行某个回调函数:

1
2
3
4
// 程序即将退出时的回调函数:
process.on('exit', function (code) {
console.log('about to exit with code: ' + code);
});

判断JavaScript执行环境

有很多JavaScript代码既能在浏览器中执行,也能在Node环境执行。可以根据浏览器和Node环境提供的全局变量名来判断。

1
2
3
4
5
if (typeof(window) === 'undefined') {
console.log('node.js');
} else {
console.log('browser');
}

fs 模块

文件系统模块,负责读写文件。fs模块同时提供了异步和同步的方法。

同步操作的好处是代码简单,缺点是程序将等待IO操作,在等待时间内,无法响应其它任何事件。而异步读取不用等待IO操作,但代码较麻烦。

比如jQuery提供的getJSON()操作:

1
2
3
4
$.getJSON('http://example.com/ajax', function (data) {
console.log('IO结果返回后执行...');
});
console.log('不等待IO结果直接执行后续代码...');

而同步的IO操作则需要等待函数返回:

1
2
// 根据网络耗时,函数将执行几十毫秒~几秒不等:
var data = getJSONSync('http://example.com/ajax');

异步读取文件

1
2
3
4
5
6
7
8
9
10
'use strict';
var fs = require('fs')

fs.readFile('sample.txt','utf-8',function(err, data){
if(err) {
console.log(err);
} else {
console.log(data)
}
})

异步读取时,传入的回调函数接收两个参数,当正常读取时,err参数为nulldata参数为读取到的String。当读取发生错误时,err参数代表一个错误对象,dataundefined。这也是Node.js标准的回调函数:第一个参数代表错误信息,第二个参数代表结果。后面我们还会经常编写这种回调函数。

读取二进制文件

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';
var fs = require('fs')

fs.readFile('wjj.jpg',function(err, data){
if(err) {
console.log(err);
} else {
var text = data.toString('utf-8'); // 把一个Buffer对象转换成String
var buf = Buffer.from(text, 'utf-8'); //把一个String转换成Buffer
console.log(buf);
console.log(data.length+'bytes');
}
})

当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。在Node.js中,Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不同)。

同步读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
var fs = require('fs')

fs.readFile('wjj.jpg',function(err, data){
if(err) {
console.log(err);
} else {
var text = data.toString('utf-8');
var buf = Buffer.from(text, 'utf-8');
console.log(buf);
console.log(data.length+'bytes');
}
})
// 同步:
var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);

结果:

1
2
3
This is a sample file.
<Buffer ef bf bd ef bf bd ef bf bd ef bf bd 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ef bf bd ef bf bd 00 43 00 0c 09 0a 0b 0a 11 0d 17 0a 17 0e 1d 0d ... 176538 more bytes>
98256bytes

捕获错误:

1
2
3
4
5
6
try {
var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);
} catch (err) {
// 出错了
}

写文件

1
2
3
4
5
6
7
8
9
10
'use strict';
var fs = require('fs');
var data = 'Hello, Node.js';
fs.writeFile('output.txt', data, function (err) {
if (err) {
console.log(err);
} else {
console.log('ok.');
}
});

类似的,writeFile()也有一个同步方法,叫writeFileSync():

1
2
3
4
'use strict';
var fs = require('fs');
var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);

stat

要获取文件大小,创建时间等信息,可以使用fs.stat(),它返回一个Stat对象,能告诉我们文件或目录的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';
var fs = require('fs');

fs.stat('sample.txt', function (err, stat) {
if (err) {
console.log(err);
} else {
// 是否是文件:
console.log('isFile: ' + stat.isFile());
// 是否是目录:
console.log('isDirectory: ' + stat.isDirectory());
if (stat.isFile()) {
// 文件大小:
console.log('size: ' + stat.size);
// 创建时间, Date对象:
console.log('birth time: ' + stat.birthtime);
// 修改时间, Date对象:
console.log('modified time: ' + stat.mtime);
}
}
});

同步 or 异步 ?

由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。

服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。

stream模块

一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。

流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。

在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var fs = require('fs');
// 以流的形式读入文件
var rs = fs.createReadStream('sample.txt', 'utf-8'); // 打开一个流
rs.on('data', function(chunk) { // 注意,data事件可能会有多次,每次传递的chunk是流的一部分数据
console.log('Data:');
console.log(chunk);
})
rs.on('end', function() {
console.log('end');
})
rs.on('error', function(err) {
console.log('error:' + err);
})
// 以流的形式写入文件
var ws1 = fs.createWriteStream('output.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end(); // 注意需要以end结束

var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream写入二进制数据...\n', 'utf-8'));
ws2.write(new Buffer('END.', 'utf-8'));
ws2.end();

所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable

pipe

一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe

1
2
3
4
5
6
var fs = require('fs');		// 一个复制文件的程序

var rs = fs.createReadStream('sample.txt');
var ws = fs.createWriteStream('copied.txt');

rs.pipe(ws);

http模块

要开发HTTP服务器程序,从头处理TCP连接,解析HTTP是,这些工作实际上已经由Node.js自带的http模块完成了。应用程序并不直接和HTTP协议打交道,而是操作http模块提供的requestresponse对象。

request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 导入http模块:
var http = require('http');

// 创建http server, 并传入回调函数
var server = http.createServer(function(request, response) {
// 回调函数接收request 和 response 对象
// 获得HTTP请求的method 和 url:
console.log(request.method + ':' + request.url);
// 将HTTP响应200写入response, 同时设置Content-Type: text/html:
response.writeHead(200, {'Content-Type': 'text/html'});
// 将HTTP响应的HTML内容写入response:
response.end('<h1>Hello world!</h1>');
})

// 让服务器监听端口
server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

命令提示符下运行该程序,可以看到以下输出:

1
2
$ node hello.js 
Server is running at http://127.0.0.1:8080/

打开浏览器输入http://localhost:8080,即可看到服务器响应的内容。

文件服务器

我们可以设定一个目录,然后让Web程序变成一个文件服务器。要实现这一点,我们只需要解析request.url中的路径,然后在本地找到对应的文件,把文件内容发送出去就可以了。

解析URL需要用到Node.js提供的url模块,通过parse()将一个字符串解析为一个Url对象:

1
2
3
4
5
6
7
8
var url = require('url');
console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));
/*
Url {
...
pathname: '/path/to/file',
path: '/path/to/file?query=string',
href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' } */

处理本地文件目录需要使用Node.js提供的path模块,它可以方便地构造目录

1
2
3
4
5
6
7
var path = require('path');

// 解析当前目录:
var workDir = path.resolve('.'); // 'C:\\Users\\86178\\Desktop\\Study\\html+css+js_code'
// 组合完整的文件路径:当前目录+'pub'+'index.html':
var filePath = path.join(workDir, 'pub', 'index.html');
// C:\\Users\\86178\\Desktop\\Study\\html+css+js_code\\pub\\index.html'

一个文件服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
'use strict';

var
fs = require('fs'),
url = require('url'),
path = require('path'),
http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');

console.log('Static root dir: ' + root);

// 创建服务器
var server = http.createServer(function(request, response) {
// 获得url的path
var pathname = url.parse(request.url).pathname;
// 获得对应的本地文件路径
var filepath = path.join(root, pathname);
// 获取文件状态
fs.stat(filepath, function(err, stats) {
if(!err && stats.isFile()) {
// 没有出错并且文件存在
console.log('200' + request.url);
// 发送200响应
response.writeHead(200);
// 将文件流向response, 由于response对象本身是一个Writable Stream,直接用pipe()方法就实现了自动读取文件内容并输出到HTTP响应
fs.createReadStream(filepath).pipe(response);
} else {
// 出错了或文件不存在
console.log('404' + request.url);
// 发送404响应
response.writeHead(404);
response.end('404 Not Found');
}
})
})

server.listen(8080);

console.log('Server is running at http://127.0.0.1:8080/');

crypto模块

目的是为了提供通用的加密和哈希算法。

MD5

给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:

1
2
3
4
5
6
7
8
9
const crypto = require('crypto');

const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544

update()方法默认字符串编码为UTF-8,也可以传入Buffer。

如果要计算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的结果1f32b9c9932c02227819a4151feed43e131aca40

还可以使用更安全的sha256sha512

Hmac

可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:

1
2
3
4
5
6
7
8
const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570...

只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。

AES

是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const crypto = require('crypto');

function aesEncrypt(data, key) {
const cipher = crypto.createCipher('aes192', key);
var crypted = cipher.update(data, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}

function aesDecrypt(encrypted, key) {
const decipher = crypto.createDecipher('aes192', key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);

console.log('Plain text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text: ' + decrypted);

AES有很多不同的算法,如aes192aes-128-ecbaes-256-cbc等,AES除了密钥外还可以指定IV(Initial Vector),不同的系统只要IV不同,用相同的密钥加密相同的数据得到的加密结果也是不同的。加密结果通常有两种表示方法:hex和base64,这些功能Nodejs全部都支持,但是在应用中要注意,如果加解密双方一方用Nodejs,另一方用Java、PHP等其它语言,需要仔细测试。如果无法正确解密,要确认双方是否遵循同样的AES算法,字符串密钥和IV是否相同,加密后的数据是否统一为hex或base64格式。

Diffie-Hellman

DH算法是一种密钥交换协议,它可以让双方在不泄漏密钥的情况下协商出一个密钥来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const crypto = require('crypto');

// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();

var prime = ming.getPrime();
var generator = ming.getGenerator();

console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));

// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();

// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_secret = hong.computeSecret(ming_keys);

// print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));

RSA

是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。