一、Node.js 回调函数
- Node.js 异步编程的直接体现就是回调函数。
- Node 所有 API 都支持回调函数。
- 回调函数一般作为函数的最后一个参数出现。
例如,我们可以一边读取文件,一边执行其他命令,在文件读取完成后,我们将文件内容作为回调函数的参数返回。这样在执行代码时就没有阻塞或等待文件 I/O 操作。这就大大提高了 Node.js 的性能,可以处理大量的并发请求。
二、Node.js 事件循环
- Node.js 是单进程单线程应用程序。
- Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。
- Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数。
- Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
- Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件,如下实例:
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();
// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);
// 触发事件
eventEmitter.emit('eventName');
三、Node.js EventEmitter
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。
Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。
EventEmitter 类
- events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。
- 当添加新的监听器时,newListener 事件会触发,当监听器被移除时,removeListener 事件被触发。
- 对于每个事件,EventEmitter 支持 若干个事件监听器。当事件触发时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。
- EventEmitter 提供了多个属性,如 on 和 emit。on 函数用于绑定事件函数,emit 属性用于触发一个事件。
- error事件:EventEmitter 定义了一个特殊的事件 error。我们一般要为会触发 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。例如:emitter.emit('error');
- 继承EventEmitter:包括fs、net、 http在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它。原因有两点:首先,具有某个实体功能的对象实现事件符合语义, 事件的监听和发生应该是一个对象的方法。其次 ,JavaScript 的对象机制是基于原型的,支持 部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。
四、Node.js Buffer(缓冲区)
- JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer类,该类用来创建一个专门存放二进制数据的缓存区。
- 一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。
- Node.js 目前支持的字符编码包括:ascii、utf8、utf16le(ucs2)、base64、latin1(binary)、hex。
const buf = Buffer.from('runoob', 'ascii');
// 输出 72756e6f6f62
console.log(buf.toString('hex'));
// 输出 cnVub29i
console.log(buf.toString('base64'));
描述 | 方法 |
---|---|
创建 Buffer 类 | 常见的有 Buffer.alloc(size[, fill[, encoding]])、Buffer.from(array)、Buffer.from(buffer) 、Buffer.from(string[, encoding])。更多详情见菜鸟教程【Buffer】。 |
写入缓冲区 | buf.write(string[, offset[, length]][, encoding])。返回值:返回实际写入的大小。如果 buffer 空间不足, 则只会写入部分字符串。 |
从缓冲区读取数据 | buf.toString([encoding[, start[, end]]])。返回值:解码缓冲区数据并使用指定的编码返回字符串。 |
将 Buffer 转换为 JSON 对象 | buf.toJSON()。当字符串化一个 Buffer 实例时,JSON.stringify() 会隐式地调用该 toJSON()。 |
缓冲区合并 | Buffer.concat(list[, totalLength])。返回值:返回一个多个成员合并的新 Buffer 对象。 |
缓冲区比较 | buf.compare(otherBuffer)。返回值:返回一个数字,表示 buf 在 otherBuffer 之前(<0),之后(>0)或相同(=0)。 |
拷贝缓冲区 | buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]])。没有返回值。 |
缓冲区裁剪 | buf.slice([start[, end]])。返回值:返回一个新的缓冲区,它和旧缓冲区指向同一块内存,但是从索引 start 到 end 的位置剪切。 |
缓冲区长度 | buf.length。返回值:返回 Buffer 对象所占据的内存长度。 |
其他方法 | buf[index]、buf.equals(otherBuffer)、buf.fill(value[, offset][, end])。 |
五、Node.js Stream(流)
1. Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
2. Stream 有四种流类型:Readable - 可读操作、Writable - 可写操作、Duplex - 可读可写操作、Transform - 操作被写入数据,然后读出结果。
3. 所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
- data - 当有数据可读时触发。
- end - 没有更多的数据可读时触发。
- error - 在接收和写入过程中发生错误时触发。
- finish - 所有数据已被写入到底层系统时触发。
4. 从流中读取数据
var fs = require("fs");
var data = '';
// 创建可读流
var readerStream = fs.createReadStream('input.txt');
// 设置编码为 utf8。
readerStream.setEncoding('UTF8');
// 处理流事件 --> data, end, and error
readerStream.on('data', function(chunk) {
data += chunk;
});
readerStream.on('end',function(){
console.log(data);
});
readerStream.on('error', function(err){
console.log(err.stack);
});
console.log("程序执行完毕");
5. 写入流
var fs = require("fs");
var data = '菜鸟教程官网地址:www.runoob.com';
// 创建一个可以写入的流,写入到文件 output.txt 中
var writerStream = fs.createWriteStream('output.txt');
// 使用 utf8 编码写入数据
writerStream.write(data,'UTF8');
// 标记文件末尾
writerStream.end();
// 处理流事件 --> data, end, finish, and error
writerStream.on('finish', function() {
console.log("写入完成。");
});
writerStream.on('error', function(err){
console.log(err.stack);
});
console.log("程序执行完毕");
6. 管道流
管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。
var fs = require("fs");
// 创建一个可读流
var readerStream = fs.createReadStream('input.txt');
// 创建一个可写流
var writerStream = fs.createWriteStream('output.txt');
// 管道读写操作
// 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
readerStream.pipe(writerStream);
console.log("程序执行完毕");
7. 链式流
链式是通过连接输出流到另外一个流并创建多个流操作链的机制。链式流一般用于管道操作。
接下来我们就是用管道和链式来压缩和解压文件。
创建 compress.js 文件, 代码如下:
var fs = require("fs");
var zlib = require('zlib');
// 压缩 input.txt 文件为 input.txt.gz
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
console.log("文件压缩完成。");
接下来,让我们来解压该文件,创建 decompress.js 文件,代码如下:
var fs = require("fs");
var zlib = require('zlib');
// 解压 input.txt.gz 文件为 input.txt
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('input.txt'));
console.log("文件解压完成。");
六、Node.js 模块系统
1. 为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。
2. 模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。
3. Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。
创建 hello.js 文件,代码如下:
exports.world = function() {
console.log('Hello World');
}
创建 main.js 文件,代码如下:
var hello = require('./hello');
hello.world();
有时候我们只是想把一个对象封装到模块中(module),例如:
//hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;
//main.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();
注:exports 和 module.exports 的使用
如果要对外暴露属性或方法,就用 exports 就行,要暴露对象(类似class,包含了很多属性和方法),就用 module.exports。
4. Node.js 的 require 方法中的文件查找策略如下:
Node.js 中存在 4 类模块(原生模块和3种文件模块),原生模块优先级更高。
七、Node.js 函数
在JavaScript中,一个函数可以作为另一个函数的参数。Node.js中函数的使用与Javascript类似。
1)先定义,再传递
function say(word) {
console.log(word);
}
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
var http = require("http");
function onRequest(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
2)匿名函数
function execute(someFunction, value) {
someFunction(value);
}
execute(function(word){ console.log(word) }, "Hello");
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);
八、Node.js 路由
我们要为路由提供请求的 URL 和其他需要的 GET 及 POST 参数,随后路由需要根据这些数据来执行相应的代码。
我们需要的所有数据都会包含在 request 对象中,该对象作为 onRequest() 回调函数的第一个参数传递。但是为了解析这些数据,我们需要额外的 Node.JS 模块,它们分别是 url 和 querystring 模块。
server.js 文件代码如下:
var http = require("http");
var url = require("url");
function start(route) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
router.js 文件代码如下:
function route(pathname) {
console.log("About to route a request for " + pathname);
}
exports.route = route;
同时,我们会相应扩展 index.js,使得路由函数可以被注入到服务器中:
var server = require("./server");
var router = require("./router");
server.start(router.route);
九、Node.js 全局对象
JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。
在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。
1. 全局对象与全局变量
global 最根本的作用是作为全局变量的宿主。按照 ECMAScript 的定义,满足以下条件的变量是全局变量:
- 在最外层定义的变量(在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的,而模块本身不是最外层上下文);
- 全局对象的属性;
- 隐式定义的变量(未定义直接赋值的变量)。
注意: 最好不要使用 var 定义变量以避免引入全局变量,因为全局变量会污染命名空间,提高代码的耦合风险。
2. 全局变量
__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。
__dirname 表示当前执行脚本所在的目录。
3. 全局函数:setTimeout(cb, ms)、clearTimeout( t ) 、setInterval(cb, ms)、clearInterval(t)。
4. console对象
console 用于提供控制台标准输出,它是由 Internet Explorer 的 JScript 引擎提供的调试工具,后来逐渐成为浏览器的实施标准。
常见的console对象方法:console.log()、console.info()、console.error()、console.warn()、console.dir()、console.time()、console.timeEnd()、console.trace()、console.assert()。
console.info("程序开始执行:");
var counter = 10;
console.log("计数: %d", counter);
console.time("获取数据");
//
// 执行一些代码
//
console.timeEnd('获取数据');
console.info("程序执行完毕。")
$ node main.js
程序开始执行:
计数: 10
获取数据: 0ms
程序执行完毕
5. process
process 是一个全局变量,即 global 对象的属性。
它用于描述当前 Node.js 进程状态的对象,提供了一个与操作系统的简单接口。
常用的process 对象成员方法:exit、beforeExit、uncaughtException、Signal 事件。
process.on('exit', function(code) {
// 以下代码永远不会执行
setTimeout(function() {
console.log("该代码不会执行");
}, 0);
console.log('退出码为:', code);
});
console.log("程序执行结束");
$ node main.js
程序执行结束
退出码为: 0
process 属性
// 输出到终端
process.stdout.write("Hello World!" + "\n");
// 通过参数读取
process.argv.forEach(function(val, index, array) {
console.log(index + ': ' + val);
});
// 获取执行路径
console.log(process.execPath);
// 平台信息
console.log(process.platform);
$ node main.js
Hello World!
0: node
1: /web/www/node/main.js
/usr/local/node/0.10.36/bin/node
darwin
process 方法
// 输出当前目录
console.log('当前目录: ' + process.cwd());
// 输出当前版本
console.log('当前版本: ' + process.version);
// 输出内存使用情况
console.log(process.memoryUsage());
$ node main.js
当前目录: /web/com/runoob/nodejs
当前版本: v0.10.36
{ rss: 12541952, heapTotal: 4083456, heapUsed: 2157056 }
更多process的属性和方法见菜鸟教程:https://www.runoob.com/nodejs/nodejs-global-object.html
十、Node.js 常用工具
util 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能过于精简的不足。
util.inherits(constructor, superConstructor) 是一个实现对象间原型继承的函数。特别注意,仅仅继承了原型。
util.inspect(object,[showHidden],[depth],[colors]) 是一个将任意对象转换为字符串的方法,通常用于调试和错误输出。
- 它至少接受一个参数 object,即要转换的对象。
- showHidden 是一个可选参数,如果值为 true,将会输出更多隐藏信息。
- depth 表示最大递归的层数,如果对象很复杂,你可以指定层数以控制输出信息的多少。如果不指定depth,默认会递归2层,指定为 null 表示将不限递归层数完整遍历对象。
- 如果 colors 值为 true,输出格式将会以 ANSI 颜色编码,通常用于在终端显示更漂亮的效果。
- 特别要指出的是,util.inspect 并不会简单地直接把对象转换为字符串,即使该对象定义了 toString 方法也不会调用。
util.isArray(object):如果给定的参数 "object" 是一个数组(如:[]、new Array())返回true,否则返回false。
util.isRegExp(object):如果给定的参数 "object" 是一个正则表达式(如:/some regexp/、new RegExp("another regexp"))返回true,否则返回false。
util.isDate(object):如果给定的参数 "object" 是一个日期(如:new Date())返回true,否则返回false(如:Date()不行)。
util.isError(object):如果给定的参数 "object" 是一个错误对象(如:new Error()、new TypeError())返回true,否则返回false。
十一、Node.js 文件系统
描述 | 方法 |
---|---|
导入文件系统模块(fs) | var fs = require("fs") |
读取文件内容 | 异步的 fs.readFile(file, callback) 和同步的 fs.readFileSync(file, callback) |
打开文件(异步模式) | fs.open(path, flags[, mode], callback) 其中,flags - 文件打开的行为,常见的有r/r+/rs/rs+/w/wx/w+/wx+/a/ax/a+/ax+。 mode - 设置文件模式(权限),文件创建默认权限为 0666(可读,可写)。 callback - 回调函数,带有两个参数如:callback(err, fd)。 |
获取文件信息(异步模式) | fs.stat(path, callback) 其中,callback - 回调函数,带有两个参数如:(err, stats), stats 是 fs.Stats 对象。 stats类中的方法有:stats.isFile()/stats.isDirectory()/stats.isBlockDevice()/ stats.isCharacterDevice()/stats.isSymbolicLink()/stats.isFIFO()/vstats.isSocket() |
写入文件(异步模式) | fs.writeFile(file, data[, options], callback) 其中,options - 该参数是一个对象,包含 {encoding, mode, flag}。默认编码为 utf8, 模式为 0666,flag 为 'w'。 writeFile 直接打开文件默认是 w 模式,所以如果文件存在,该方法写入的内容会覆盖旧的文件内容。 |
追加文件内容(异步模式) | fs.appendFile(filename, data[, options], callback) |
读取文件(异步模式) | fs.read(fd, buffer, offset, length, position, callback) 其中,fd - 通过 fs.open() 方法返回的文件描述符。 buffer - 数据写入的缓冲区。 offset - 缓冲区写入的写入偏移量。 length - 要从文件中读取的字节数。 position - 文件读取的起始位置,如果 position 的值为 null,则会从当前文件指针的位置读取。 callback - 回调函数,有三个参数err, bytesRead, buffer,err 为错误信息, bytesRead 表示读取的字节数,buffer 为缓冲区对象。 |
关闭文件(异步模式) | fs.close(fd, callback) |
截取文件(异步模式) | fs.ftruncate(fd, len, callback) 其中,len - 文件内容截取的长度。 |
删除文件 | fs.unlink(path, callback) |
创建目录 | fs.mkdir(path[, options], callback) 其中,options 参数可以是: recursive - 是否以递归的方式创建目录,默认为 false。 mode - 设置目录权限,默认为 0777。 |
读取目录 | fs.readdir(path, callback) 其中,callback - 回调函数,回调函数带有两个参数err, files,err 为错误信息,files 为 目录下的文件数组列表。 |
删除目录 | fs.rmdir(path, callback) |
十二、Node.js GET/POST请求
1. 获取 GET 请求内容:
var http = require('http');
var url = require('url');
var util = require('util');
http.createServer(function(req, res){
res.writeHead(200, {'Content-Type': 'text/plain'});
// 解析 url 参数
var params = url.parse(req.url, true).query;
res.write("网站名:" + params.name);
res.write("\n");
res.write("网站 URL:" + params.url);
res.end();
}).listen(3000);
2. 获取 POST 请求内容
var http = require('http');
var querystring = require('querystring');
var postHTML =
'<html><head><meta charset="utf-8"><title>菜鸟教程 Node.js 实例</title></head>' +
'<body>' +
'<form method="post">' +
'网站名: <input name="name"><br>' +
'网站 URL: <input name="url"><br>' +
'<input type="submit">' +
'</form>' +
'</body></html>';
http.createServer(function (req, res) {
var body = "";
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function () {
// 解析参数
body = querystring.parse(body); //在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
// 设置响应头部信息及编码
res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'});
if(body.name && body.url) { // 输出提交的数据
res.write("网站名:" + body.name);
res.write("<br>");
res.write("网站 URL:" + body.url);
} else { // 输出表单
res.write(postHTML);
}
res.end();
});
}).listen(3000);
END