Node.js 学习提纲

留给自己参考的Node.js学习提纲

目录:

  1. HelloWorld
  2. 回调函数
  3. 事件循环
  4. EventEmitter
  5. 模块系统
  6. web 模块
  7. express 框架
  8. Restful API
  9. MySQL
  10. Buffer/Stream
  11. 全局对象
  12. fs模块
  13. util模块
  14. 其余常用模块

HelloWorld

过程

新建一个 server.js 文件,输入以下代码:

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

http.createServer(function (request, response) {

// 发送 HTTP 头部
// HTTP 状态值: 200 : OK
// 内容类型: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'});

// 发送响应数据 "Hello World"
response.end('Hello World\n');
}).listen(8888);

// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');

然后在命令行使用 node 命令执行以上代码:

1
node server.js

之后访问本地的 8888 端口,就可以看到我们的 HelloWorld 消息了。

1
http://localhost:8888/

代码分析

从上面的代码中,我们可以看到,首先代码 require 了一个名为 http 的模块,用来提供 http 服务

之后使用 http.createServer 方法,传入了一个 function 作为回调方法,用来处理监听到的请求,之后在 8888 端口启动服务,监听该端口的请求。

在回调方法中可以看到,我们在响应中定义了响应头,其中响应的 http 状态码是200,内容类型是 text/plain,之后在响应中使用 end 方法写入数据并返回

回调函数

Node.js 异步编程的直接体现就是回调。

异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。

回调函数在完成任务后就会被调用,Node 使用了大量的回调函数,Node 所有 API 都支持回调函数。

在之前学过的 ES6 Promise 中,大家一定深有体会。

回调函数一般作为函数的最后一个参数出现。

阻塞代码(同步)

1
2
3
4
5
6
var fs = require("fs");

var data = fs.readFileSync('input.txt');

console.log(data.toString());
console.log("程序执行结束!");

执行结果:

1
2
3
4
$ node main.js
本站地址:ringoer.com

程序执行结束!

可见程序是强顺序执行的,不到读取完毕文件,不会继续执行。

非阻塞代码(异步)

1
2
3
4
5
6
7
8
var fs = require("fs");

fs.readFile('input.txt', function (err, data) {
if (err) return console.error(err);
console.log(data.toString());
});

console.log("程序执行结束!");

执行结果:

1
2
3
$ node main.js
程序执行结束!
本站地址:ringoer.com

可见程序是先执行完当前任务,再在读取文件完成的时候打印文件内容的。

以上两个实例我们了解了阻塞与非阻塞调用的不同。

第一个实例在文件读取完后才执行程序, 第二个实例我们不需要等待文件读取完,这样就可以在读取文件时同时执行接下来的代码,大大提高了程序的性能。

因此,阻塞是按顺序执行的,而非阻塞是不需要按顺序的,所以如果需要处理回调函数的参数,我们就需要写在回调函数内。

为什么会先执行当前任务呢?我们将在下节介绍。

事件循环

众所周知,JavaScript 是单线程的,Node.js 也是单进程单线程应用程序,那么他是怎么提高运行效率的呢?

答案就是 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以效率相比普通的单线程大大提高。

事件驱动程序

Node.js 使用事件驱动模型,每当 web server 接收到一个新的请求,触发了对应的事件,就会将请求放入事件队列中,然后继续按队列顺序处理。

这个模型非常高效可扩展性非常强,因为 web server 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)

事件发生流程一般如下:

  1. 将回调函数绑定到事件上
  2. 触发某个事件
  3. 将回调函数放入事件队列中
  4. 按队列顺序执行

我们可以通过内置模块 events 来模拟这个事件的流程。

1
2
3
4
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

以下程序绑定事件处理程序:

1
2
// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);

我们可以通过程序触发事件:

1
2
// 触发事件
eventEmitter.emit('eventName');

关于 events 模块,将在下一节详细介绍。

EventEmitter

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。

Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。

EventEmitter类

Node.js 内置模块中含有一个名为 events 的模块,该模块向外 exports 了一个 events 对象,该对象包含一个名为 EventEmitter 的类。我们可以通过 require 命令来引入它。

1
2
3
4
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

接下来我们来解析一下上篇提到的代码。

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
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

// 创建事件处理程序
var connectHandler = function connected() {
console.log('连接成功。');

// 触发 data_received 事件
eventEmitter.emit('data_received');
}

// 绑定 connection 事件处理程序
eventEmitter.on('connection', connectHandler);

// 使用匿名函数绑定 data_received 事件
eventEmitter.on('data_received', function(){
console.log('数据接收成功。');
});

// 设置定时器在触发事件前
setTimeout(()=>console.log('定时器执行成功。'),0);

// 触发 connection 事件
eventEmitter.emit('connection');

console.log("程序执行完毕。");

执行结果:

1
2
3
4
5
$ node main.js
连接成功。
数据接收成功。
程序执行完毕。
定时器执行成功。

在上述代码中,我们创建了一个事件发生器,并通过 on 命令指定了两个事件及其回调,然后通过 emit 命令触发指定事件。

我们观察到,尽管先访问到了 setTimeout,但是其结果却在最后被打印,说明 eventEmitter 的执行逻辑与 setTimeout 并不相同。通过阅读源码,可知 eventEmitter 的执行方式类似于方法调用,此处暂略。

on 和 emit

上文代码中我们用到了 on 和 emit 两个命令。现在来看看他们的方法原型。

1
2
on(event,listener)
event 要求为一个字符串,listener 要求为一个回调函数,表示在事件 event 的回调函数列表尾部插入 listener
1
2
emit(event,[data])
event 要求为一个字符串,[data] 为可选项,表示触发事件 event,将按顺序执行 event 回调函数列表中的每个回调函数,并向每个函数中都传入 data

这是 EventEmitter 两个最为重要的方法。

其余方法如下:

1
2
addListener(event, listener)
为指定事件添加一个监听器到监听器数组的尾部。与on命令相同。
1
2
once(event, listener)
为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。
1
2
3
4
removeListener(event, listener)
移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。

它接受两个参数,第一个是事件名称,第二个是回调函数名称。
1
2
removeAllListeners([event])
移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器。
1
2
setMaxListeners(n)
默认情况下, EventEmitters 如果你添加的监听器超过 10 个就会输出警告信息。 setMaxListeners 函数用于提高监听器的默认限制的数量。
1
2
listeners(event)
返回指定事件的监听器数组。
1
2
listenerCount(event)
返回指定事件的监听器数量。

以上是 EventEmitter 的方法,可以设置自定义事件。

EventEmitter 还有内置的事件。

1
2
newListener
该事件在添加新监听器时被触发。
1
2
removeListener
该事件在删除一个监听器时被触发。
1
2
error
该事件在发生错误时被触发。若没有为该事件绑定监听器,则程序会直接出错退出。

同步调用

上文说到,eventEmitter 的执行逻辑与 setTimeout 并不相同。那么具体是如何执行的呢?我们需要阅读 eventEmitter 的源码,或者自己手写一个。此处选择手写一个作为示例。

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
class EventEmitter {
constructor() {
// 事件对象,存放订阅的名字和事件 如: { click: [ handle1, handle2 ] }
this.events = {}
}
// 订阅事件的方法
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [callback]
} else {
this.events[eventName].push(callback)
}
}
// 触发事件的方法
emit(eventName, ...rest) {
this.events[eventName] &&
this.events[eventName].forEach(f => f.apply(this, rest))
}
// 移除订阅事件
remove(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(f => f != callback)
}
}
// 只执行一次订阅的事件,然后移除
once(eventName, callback) {
// 绑定的时fn, 执行的时候会触发fn函数
const fn = () => {
callback() // fn函数中调用原有的callback
this.remove(eventName, fn) // 删除fn, 再次执行的时候之后执行一次
}
this.on(eventName, fn)
}
}

可见,eventEmitter 是以类似方法调用的方式传递触发信号的,然后又以数组顺序为基础来进行顺序执行,所以整体上是同步执行的,不是 setTimeout 一样的异步执行。

比起一般的方法调用, eventEmitter 胜在一个入口可以调用多个(同一数组内)的方法。

模块系统

模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。在模块系统的帮助下,Node.js的文件可以相互调用。

模块系统有两个重要动作:导入(require)和导出(exports)。

require

我们可以通过 require 命令来引入一个模块,例如:

1
2
var hello = require('./hello');
hello.world();

在 require 之后,可以对这个模块做出哪些操作,则取决于这个模块 exports 了什么东西。

exports

对于任何一个模块,都可以通过操作内置对象 exports 来导出内容。

例如导出一个 function world,可以通过两种方案来实现。

  1. exports.world = function (){}
  2. module.exports = function (){}

第一种方案,在 require 之后,主模块引入了一个来自从模块的对象,该对象包含一个名为 world 的方法。

第二种方案,在 require 之后,主模块只引入了一个方法,该方法的名字取决于主模块中所起的变量名。

一般不建议两种方案同时使用,因为阅读 require 源码可以发现,在 require 一个模块的时候,会先执行 exports.xxx 的赋值,最后才执行 module.exports 的赋值,这使得之前所做的赋值被完全覆盖了,只有最后的赋值才有效。

模块分类

之前的章节中,我们使用了 http 模块,event 模块等。事实上,JavaScript 有 4 种模块,分为原生模块和 3 种文件模块,通过在 require 命令中指定的字符串不同而导入。

  • http、fs、path等,原生模块。
  • ./mod或../mod,相对路径的文件模块。
  • /pathtomodule/mod,绝对路径的文件模块。
  • mod,非原生模块的文件模块。

由于有多种模块,所以导入模块也有对应的优先级,如图。

模块加载优先级

web 模块

使用 Node.js 创建 web 客户端需要引入 http 模块。

我们可以用 Node.js 搭建一个 web 服务器与一个客户端。

服务器代码

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
var http = require('http');
var fs = require('fs');
var url = require('url');


// 创建服务器
http.createServer( function (request, response) {
// 解析请求,包括文件名
var pathname = url.parse(request.url).pathname;

// 输出请求的文件名
console.log("Request for " + pathname + " received.");

// 从文件系统中读取请求的文件内容
fs.readFile(pathname.substr(1), function (err, data) {
if (err) {
console.log(err);
// HTTP 状态码: 404 : NOT FOUND
// Content Type: text/html
response.writeHead(404, {'Content-Type': 'text/html'});
}else{
// HTTP 状态码: 200 : OK
// Content Type: text/html
response.writeHead(200, {'Content-Type': 'text/html'});

// 响应文件内容
response.write(data.toString());
}
// 发送响应数据
response.end();
});
}).listen(8080);

// 控制台会输出以下信息
console.log('Server running at http://127.0.0.1:8080/');

需要在服务器文件目录下创建一个 index.html 文件来配合。

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
<h1>我的第一个标题</h1>
<p>我的第一个段落。</p>
</body>
</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
var http = require('http');

// 用于请求的选项
var options = {
host: 'localhost',
port: '8080',
path: '/index.html'
};

// 处理响应的回调函数
var callback = function(response){
// 不断更新数据
var body = '';
response.on('data', function(data) {
body += data;
});

response.on('end', function() {
// 数据接收完成
console.log(body);
});
}
// 向服务端发送请求
var req = http.request(options, callback);
req.end();

Express 框架

Express 是一个基于 Node.js 的 web 应用框架。

Express 框架的核心特性如下:

  • 可以设置中间件来响应 HTTP 请求。
  • 定义了路由表用于执行不同的 HTTP 请求动作。
  • 可以通过向模板传递参数来动态渲染 HTML 页面。

express 可以通过 require(‘express’) 引入,暴露的是一个方法。使用例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//express_demo.js 文件
var express = require('express');
var app = express();

app.get('/', function (req, res) {
res.send('Hello World');
})

var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("应用实例,访问地址为 http://%s:%s", host, port)

})

从上述代码中,可以看到 app 有两个关键方法:get 和 listen

  1. get(url, func(request, response)),表示使用 http get 请求访问,第一个参数是访问的路径(可以是一个正则表达式,或是 Rest 风格的 url 字符串,详见下章),第二个参数是这个事件发生时的回调。其中回调含有 2 个参数:request 和 response,分别表示这个 http 请求的请求包体和响应包体。
  2. listen(port, func),返回一个 app 实例,监听来自端口 port 的所有请求,并在服务成功启动后调用指定的回调函数。

在 request 对象和 response 对象上,各自还有自己的对象方法。

Request 对象 - request 对象表示 HTTP 请求,包含了请求查询字符串,参数,内容,HTTP 头部等属性。常见属性有:

  1. req.app:当callback为外部文件时,用req.app访问express的实例
  2. req.baseUrl:获取路由当前安装的URL路径
  3. req.body / req.cookies:获得「请求主体」/ Cookies
  4. req.fresh / req.stale:判断请求是否还「新鲜」
  5. req.hostname / req.ip:获取主机名和IP地址
  6. req.originalUrl:获取原始请求URL
  7. req.params:获取路由的parameters
  8. req.path:获取请求路径
  9. req.protocol:获取协议类型
  10. req.query:获取URL的查询参数串
  11. req.route:获取当前匹配的路由
  12. req.subdomains:获取子域名
  13. req.accepts():检查可接受的请求的文档类型
  14. req.acceptsCharsets / req.acceptsEncodings / req.acceptsLanguages:返回指定字符集的第一个可接受字符编码
  15. req.get():获取指定的HTTP请求头
  16. req.is():判断请求头Content-Type的MIME类型

Response 对象 - response 对象表示 HTTP 响应,即在接收到请求时向客户端发送的 HTTP 响应数据。常见属性有:

  1. res.app:同req.app一样
  2. res.append():追加指定HTTP头
  3. res.set()在res.append()后将重置之前设置的头
  4. res.cookie(name,value [,option]):设置Cookie
  5. opition: domain / expires / httpOnly / maxAge / path / secure / signed
  6. res.clearCookie():清除Cookie
  7. res.download():传送指定路径的文件
  8. res.get():返回指定的HTTP头
  9. res.json():传送JSON响应
  10. res.jsonp():传送JSONP响应
  11. res.location():只设置响应的Location HTTP头,不设置状态码或者close response
  12. res.redirect():设置响应的Location HTTP头,并且设置状态码302
  13. res.render(view,[locals],callback):渲染一个view,同时向callback传递渲染后的字符串,如果在渲染过程中有错误发生next(err)将会被自动调用。callback将会被传入一个可能发生的错误以及渲染后的页面,这样就不会自动输出了。
  14. res.send():传送HTTP响应
  15. res.sendFile(path [,options] [,fn]):传送指定路径的文件 -会自动根据文件extension设定Content-Type
  16. res.set():设置HTTP头,传入object可以一次设置多个头
  17. res.status():设置HTTP状态码
  18. res.type():设置Content-Type的MIME类型

静态文件

可以使用形如 app.use('/public', express.static('public')); 的代码来设置静态文件路径。

该代码使用了 use 方法,该方法需要两个参数。

  1. url,表示 http 请求需要访问的路径
  2. express.static(localpath),其中 localpath 表示存放静态文件的文件夹的本地路径

比如此时我在 public 文件夹下存放了一张图片,名为 hello.png,则我访问 http://localhost:port/public/hello.png 时,页面上就会返回这张图片。

Restful API

Restful API 是用 http 动词描述动作,用 URL 定位资源的 API 设计风格。在 Express 框架中,我们可以使用 Restful 默认的 4 个动词来定义 app 监听。

  1. app.get(url, func(request, response))
  2. app.post(url, func(request, response))
  3. app.put(url, func(request, response))
  4. app.delete(url, func(request, response))

其中,绑定 url 时,要绑定指定的 id,可以使用形如 /user/:id 的 url,来访问路径上的动态参数。该写法在前端类似于 vue 的 router 绑定,在后端类似于 spring 的 @PathVariable

MySQL

Node.js 既然可以写后端,自然也可以连接 MySQL。容易想到,我们可以通过 require(‘mysql’) 来引入 mysql模块,不过需要先 npm install mysql

连接数据库

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
var mysql  = require('mysql');  

var connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : '123456',
port: '3306',
database: 'test'
});

connection.connect();

var addSql = 'INSERT INTO websites(Id,name,url,alexa,country) VALUES(0,?,?,?,?)';
var addSqlParams = ['菜鸟工具', 'https://c.runoob.com','23453', 'CN'];
//增
connection.query(addSql,addSqlParams,function (err, result) {
if(err){
console.log('[INSERT ERROR] - ',err.message);
return;
}

console.log('--------------------------INSERT----------------------------');
//console.log('INSERT ID:',result.insertId);
console.log('INSERT ID:',result);
console.log('-----------------------------------------------------------------\n\n');
});

connection.end();

通过向 createConnection 传入一个对象作为参数,来配置连接的必要参数。

之后可以通过 query 方法,来提交 sql 请求。query 方法包含 3 个参数。

query(sqlString, sqlParams, func(err, result))

在 sqlString 中,可以留若干个?作为占位符,?会按顺序匹配 sqlParams 中的参数。

sql 请求成功返回后,使用 func 作为回调函数。该回调函数含有 2 个参数,第一个是报错信息,第二个是 sql 执行结果。若没有出错,则 err 为空。

sql 请求的返回值与 spring mapper 类似,select、insert 返回具体数据,update、delete 返回的 result 对象中只包含一个 affectedRows 字段,表示影响的行数。

Buffer/Stream

Buffer

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。

但在处理像 TCP 流或文件流时,必须使用到二进制数据。因此在 Node.js 中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

Buffer 在使用时需要指定字符编码。支持的编码如下:

  1. ascii - 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
  2. utf8 - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。
  3. utf16le - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  4. ucs2 - utf16le 的别名。
  5. base64 - Base64 编码。
  6. latin1 - 一种把 Buffer 编码成一字节编码的字符串的方式。
  7. binary - latin1 的别名。
  8. hex - 将每个字节编码为两个十六进制字符。

Buffer 具有类似数组的特性。重要的 API 如下。

创建 Buffer

  1. Buffer.alloc(size[, fill[, encoding]])

    返回一个指定大小的 Buffer 实例,如果没有设置 fill,则默认填满 0

  2. Buffer.allocUnsafe(size)

    返回一个指定大小的 Buffer 实例,但是它不会被初始化,所以它可能包含敏感的数据

  3. Buffer.allocUnsafeSlow(size)

  4. Buffer.from(array)

    返回一个被 array 的值初始化的新的 Buffer 实例(传入的 array 的元素只能是数字,不然就会自动被 0 覆盖)

  5. Buffer.from(arrayBuffer[, byteOffset[, length]])

    返回一个新建的与给定的 ArrayBuffer 共享同一内存的 Buffer。

  6. Buffer.from(buffer)

    复制传入的 Buffer 实例的数据,并返回一个新的 Buffer 实例

  7. Buffer.from(string[, encoding])

    返回一个被 string 的值初始化的新的 Buffer 实例

写入 Buffer

1
buf.write(string[, offset[, length]][, encoding])

参数

参数描述如下:

  • string - 写入缓冲区的字符串。
  • offset - 缓冲区开始写入的索引值,默认为 0 。
  • length - 写入的字节数,默认为 buffer.length
  • encoding - 使用的编码。默认为 ‘utf8’ 。

根据 encoding 的字符编码写入 string 到 buf 中的 offset 位置。 length 参数是写入的字节数。 如果 buf 没有足够的空间保存整个字符串,则只会写入 string 的一部分。 只部分解码的字符不会被写入。

返回值

返回实际写入的大小。如果 buffer 空间不足, 则只会写入部分字符串。

实例

1
2
3
4
buf = Buffer.alloc(256);
len = buf.write("www.runoob.com");

console.log("写入字节数 : "+ len);

执行以上代码,输出结果为:

1
2
$node main.js
写入字节数 : 14

其余与数组方法基本相同,详见 Node.js Buffer(缓冲区) | 菜鸟教程

Stream

Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。

Node.js,Stream 有四种流类型:

  • Readable - 可读操作。
  • Writable - 可写操作。
  • Duplex - 可读可写操作.
  • Transform - 操作被写入数据,然后读出结果。

所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

  • data - 当有数据可读时触发。
  • end - 没有更多的数据可读时触发。
  • error - 在接收和写入过程中发生错误时触发。
  • finish - 所有数据已被写入到底层系统时触发。

主要有三种流:

  • 写入流
  • 管道流
  • 链式流

详见 Node.js Stream(流) | 菜鸟教程

11-14 章 待写

待续……

参考文章

https://www.runoob.com/nodejs/nodejs-tutorial.html

https://blog.csdn.net/qq_39953537/article/details/102685328

--It's the end.Thanks for your read.--