因为Node.js是运行在服务区端的JavaScript环境,服务器程序和浏览器程序相比,最大的特点是没有浏览器的安全限制了,
而且,服务器程序必须能接收网络请求,读写文件,处理二进制内容,所以,Node.js内置的常用模块就是为了实现基本的服务器功能。
这些模块在浏览器环境中是无法被执行的,因为它们的底层代码是用C/C++在Node.js运行环境中实现的。
一、全局模块
global:全局对象
process:当前Node.js进程
JavaScript程序是由事件驱动执行的单线程模型,Node.js也不例外。Node.js不断执行响应事件的JavaScript函数,直到没有任何响应事件的函数可以执行时,Node.js就退出了。
在下一次事件响应中执行代码:process.nextTick()
( 不是立刻执行,而是要等到下一次事件循环 )
// process.nextTick()将在下一轮事件循环中调用:
process.nextTick(function () {
console.log('nextTick callback!');
});
即将退出时执行某个回调函数:exit
// 程序即将退出时的回调函数:
process.on('exit', function (code) {
console.log('about to exit with code: ' + code);
});
判断JavaScript执行环境:
根据浏览器和Node环境提供的全局变量名称来判断
if (typeof(window) === 'undefined') {
console.log('node.js');
} else {
console.log('browser');
}
二、fs
Node.js内置的fs模块就是文件系统模块,负责读写文件。
不同的是,fs模块同时提供了异步和同步的方法。
同步操作的好处是代码简单,缺点是程序将等待IO操作,在等待时间内,无法响应其它任何事件。
而异步读取不用等待IO操作,但代码较麻烦(通过回调函数)。
- 异步读文件:
fs.readFile()
var fs = require('fs');
fs.readFile('sample.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
-
sample.txt文件必须在当前目录下,且文件编码为utf-8
-
正常读取时,err参数为null,data参数为读取到的String。
读取发生错误时,err参数代表一个错误对象,data为und在这里插入代码片
efined。
Node.js标准的回调函数:第一个参数错误信息,第二个参数结果。
- 读取二进制文件
var fs = require('fs');
fs.readFile('sample.png', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
console.log(data.length + ' bytes');
}
});
不传入文件编码时,data返回一个Buffer
对象。
Buffer
对象就是一个包含零或任意个字节的数组(和Array不同)。
Buffer对象可以和String作转换
// Buffer -> String
var text = data.toString('utf-8');
console.log(text);
// String -> Buffer
var buf = Buffer.from(text, 'utf-8');
console.log(buf);
- 同步读文件:
fs.readFileSync()
同步读取的函数和异步函数相比,多了一个Sync
后缀
var fs = require('fs');
var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);
- 如果同步读取文件发生错误,则需要用try…catch捕获该错误:
- 异步写文件:
fs.writeFile()
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.');
}
});
如果传入的数据是String,默认按UTF-8编码写入文本文件,如果传入的参数是Buffer,则写入的是二进制文件。
- 同步写文件:
fs.writeFileSync()
var fs = require('fs');
var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);
- 获取文件或目录的详细信息:
fs.stat()
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);
}
}
});
- 对应的同步函数:
statSync()
- 同步和异步
绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码。否则,同步代码在执行时期,服务器将停止响应。
服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。
三、stream
仅在服务区端可用的模块,目的是支持“流”这种数据结构。
- 流是一种抽象的数据结构。
- 特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。
在Node.js中,流也是一个对象,响应流的事件:
data事件:流的数据已经可以读取了,
end事件:这个流已经到末尾了,没有数据可以读取了,
error事件:出错了。
var fs = require('fs');
// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');
rs.on('data', function (chunk) {
console.log('DATA:')
console.log(chunk);
});
rs.on('end', function () {
console.log('END');
});
rs.on('error', function (err) {
console.log('ERROR: ' + err);
});
- data事件可能会有多次,每次传递的chunk是流的一部分数据。
要以流的形式写入文件,只需要不断调用write()
方法,最后以end()
结束
var fs = require('fs');
var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.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
。
Readable流有一个pipe()
方法。用pipe()把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序。
var fs = require('fs');
var rs = fs.createReadStream('sample.txt');
var ws = fs.createWriteStream('copied.txt');
rs.pipe(ws)
五、http
- HTTP协议
HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。 - HTTP服务器
应用程序并不直接和HTTP协议打交道,而是操作http模块提供的request和response对象。
request
对象封装HTTP请求,调用request
的属性和方法拿到HTTP请求的信息;response
对象封装HTTP响应,操作response
对象的方法把HTTP响应返回给浏览器。
用Node.js实现一个HTTP服务器程序
// 导入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>');
});
// 让服务器监听8080端口:
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');
- 文件服务器
可以设定一个目录,让Web程序变成一个文件服务器:
解析request.url
中的路径,然后在本地找到对应的文件,把文件内容发送出去。
解析URL需要用到Node.js提供的url
模块,通过parse()将一个字符串解析为一个Url对象
var url = require('url');
console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));
//解析结果:
Url {
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
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
模块,它可以方便地构造目录
var path = require('path');
// 解析当前目录:
var workDir = path.resolve('.'); // '/Users/michael'
// 组合完整的文件路径:当前目录+'pub'+'index.html':
var filePath = path.join(workDir, 'pub', 'index.html');
// '/Users/michael/pub/index.html'
最终实现:
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,类似 '/css/bootstrap.css':
var pathname = url.parse(request.url).pathname;
// 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
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:
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/');
- 没有必要手动读取文件内容。由于response对象本身是一个Writable Stream,直接用
pipe()
方法就实现了自动读取文件内容并输出到HTTP响应。 - 在浏览器输入
http://localhost:8080/
时,会返回404,原因是程序识别出HTTP请求的不是文件,而是目录
六、crypto
目的是为了提供通用的加密和哈希算法
用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。
Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。
- MD5和SHA1
MD5是一种常用的哈希算法,用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示
const crypto = require('crypto');
const hash = crypto.createHash('md5');
// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544
如果要计算SHA1,只需要把'md5'
改成'sha1'
。
还可以使用更安全的sha256
和sha512
。
- Hmac
Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥。
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
AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用。
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);
AES有很多不同的算法,如aes192,aes-128-ecb,aes-256-cbc等
- Diffie-Hellman
DH算法是一种密钥交换协议,它可以让双方在不泄漏密钥的情况下协商出一个密钥来。 - RSA
RSA算法是一种非对称加密算法,即由一个私钥和一个公钥构成的密钥对,通过私钥加密,公钥解密,或者通过公钥加密,私钥解密。其中,公钥可以公开,私钥必须保密。